@amaster.ai/components-templates 1.5.0 → 1.6.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 (39) 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 +21 -22
  11. package/components/ai-assistant/template/components/chat-messages.tsx +10 -2
  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 +1 -1
  16. package/components/ai-assistant/template/hooks/useVoiceInput.ts +18 -20
  17. package/components/ai-assistant/template/inline-ai-assistant.tsx +1 -1
  18. package/components/ai-assistant-taro/amaster.config.json +3 -0
  19. package/components/ai-assistant-taro/package.json +94 -0
  20. package/components/ai-assistant-taro/template/components/ChatAssistantMessage.tsx +154 -0
  21. package/components/ai-assistant-taro/template/components/ChatHeader.tsx +27 -0
  22. package/components/ai-assistant-taro/template/components/ChatInput.tsx +204 -0
  23. package/components/ai-assistant-taro/template/components/ChatMessages.tsx +126 -0
  24. package/components/ai-assistant-taro/template/components/ChatUserMessage.tsx +25 -0
  25. package/components/ai-assistant-taro/template/components/VoiceInput.tsx +169 -0
  26. package/components/ai-assistant-taro/template/components/markdown.tsx +156 -0
  27. package/components/ai-assistant-taro/template/hooks/useConversation.ts +787 -0
  28. package/components/ai-assistant-taro/template/hooks/useSafeArea.ts +20 -0
  29. package/components/ai-assistant-taro/template/hooks/useVoiceInput.ts +204 -0
  30. package/components/ai-assistant-taro/template/i18n.ts +157 -0
  31. package/components/ai-assistant-taro/template/index.config.ts +10 -0
  32. package/components/ai-assistant-taro/template/index.tsx +83 -0
  33. package/components/ai-assistant-taro/template/types.ts +58 -0
  34. package/package.json +5 -2
  35. package/packages/cli/dist/index.js +14 -3
  36. package/packages/cli/dist/index.js.map +1 -1
  37. package/packages/cli/package.json +1 -1
  38. package/components/ai-assistant/example.md +0 -34
  39. package/components/ai-assistant/others.md +0 -16
@@ -0,0 +1,154 @@
1
+ import { View } from "@tarojs/components";
2
+ import { useState } from "react";
3
+ import { useAiAssistantI18n } from "../i18n";
4
+ import type {
5
+ MessagesItem,
6
+ TextMessage,
7
+ ThoughtMessage,
8
+ ToolMessage,
9
+ } from "../types";
10
+ import Markdown from "./markdown";
11
+
12
+ interface MessageCommonProps {
13
+ isNewest?: boolean;
14
+ isLoading?: boolean;
15
+ }
16
+
17
+ export const ChatLoading: React.FC<
18
+ { className?: string } & MessageCommonProps
19
+ > = ({ className }) => {
20
+ return (
21
+ <View className={`flex items-center gap-1 ${className}`}>
22
+ <View className="h-2 w-2 rounded-full bg-primary animate-pulse" />
23
+ <View
24
+ className="h-2 w-2 rounded-full bg-primary animate-pulse"
25
+ style={{ animationDelay: "150ms" }}
26
+ />
27
+ <View
28
+ className="h-2 w-2 rounded-full bg-primary animate-pulse"
29
+ style={{ animationDelay: "300ms" }}
30
+ />
31
+ </View>
32
+ );
33
+ };
34
+
35
+ const ChatTextMessage: React.FC<{ message: TextMessage }> = ({ message }) => {
36
+ if (!message.content) {
37
+ return <ChatLoading className="ml-2" />;
38
+ }
39
+
40
+ return (
41
+ <View className="whitespace-pre-wrap leading-relaxed break-words overflow-hidden min-w-0 max-w-full px-1">
42
+ <Markdown content={message.content} />
43
+ </View>
44
+ );
45
+ };
46
+
47
+ const ChatThoughtMessage: React.FC<
48
+ { message: ThoughtMessage } & MessageCommonProps
49
+ > = ({ message, isNewest, isLoading }) => {
50
+ const [expanded, setExpanded] = useState(false);
51
+ const { t } = useAiAssistantI18n();
52
+ const thinking = isLoading && isNewest;
53
+ return (
54
+ <View className="leading-relaxed whitespace-pre-wrap break-words text-lg overflow-hidden text-left">
55
+ <View
56
+ className="hover:border-border border bg-primary/50 text-primary-foreground px-2 py-1 rounded-xl inline-flex items-center gap-1 cursor-pointer"
57
+ onClick={() => setExpanded(!expanded)}
58
+ >
59
+ {thinking ? (
60
+ <View className="i-lucide-loader-circle text-blue-600 animate-spin size-4" />
61
+ ) : !expanded ? (
62
+ <View className="i-lucide-chevron-down size-4" />
63
+ ) : (
64
+ <View className="i-lucide-chevron-up size-4" />
65
+ )}
66
+ <View>{thinking ? t.thinking : t.thinkResult}</View>
67
+ </View>
68
+ {(expanded || thinking) && (
69
+ <Markdown
70
+ content={message.thought || "..."}
71
+ className="font-normal opacity-55 pl-4 py-2 border-l-[2px] ml-4"
72
+ />
73
+ )}
74
+ </View>
75
+ );
76
+ };
77
+
78
+ const ChatToolMessage: React.FC<
79
+ { message: ToolMessage } & MessageCommonProps
80
+ > = ({ message, isLoading }) => {
81
+ const status = message.toolStatus || "executing";
82
+ const { t } = useAiAssistantI18n();
83
+ return (
84
+ <View className="leading-relaxed whitespace-pre-wrap break-words flex items-center gap-1 border px-2 p-1 rounded-xl bg-muted text-lg max-w-full overflow-hidden">
85
+ {status === "success" ? (
86
+ <View className="i-lucide-badge-check size-4 text-green-600 fill-current shrink-0" />
87
+ ) : status === "failed" || status === "error" ? (
88
+ <View className="i-lucide-info size-4 text-red-600 shrink-0" />
89
+ ) : isLoading ? (
90
+ <View className="i-lucide-refresh-cw size-4 text-primary animate-spin shrink-0 text-warning" />
91
+ ) : (
92
+ <View className="i-lucide-circle-x size-4 text-orange-600 shrink-0" />
93
+ )}
94
+ <View className="flex-1 truncate flex-shrink-0">
95
+ {message.toolName || t.unknownTool}
96
+ </View>
97
+ </View>
98
+ );
99
+ };
100
+
101
+ const ChatErrorMessage: React.FC<
102
+ { message: TextMessage } & MessageCommonProps
103
+ > = ({ message }) => {
104
+ const { t } = useAiAssistantI18n();
105
+ return (
106
+ <View className="leading-relaxed whitespace-pre-wrap text-left break-words gap-1 border px-2 p-1 rounded-xl text-destructive text-lg max-w-full overflow-hidden">
107
+ {message.content || t.errorMessage}
108
+ </View>
109
+ );
110
+ };
111
+
112
+ const MessageContentRenderer: React.FC<
113
+ {
114
+ message: MessagesItem;
115
+ } & MessageCommonProps
116
+ > = ({ message, ...rest }) => {
117
+ switch (message.kind) {
118
+ case "text-content":
119
+ return <ChatTextMessage message={message as TextMessage} {...rest} />;
120
+ case "thought":
121
+ return (
122
+ <ChatThoughtMessage message={message as ThoughtMessage} {...rest} />
123
+ );
124
+ case "tool":
125
+ return <ChatToolMessage message={message as ToolMessage} {...rest} />;
126
+ case "error":
127
+ return <ChatErrorMessage message={message as TextMessage} {...rest} />;
128
+ default:
129
+ return null;
130
+ }
131
+ };
132
+
133
+ export const ChatAssistantMessage: React.FC<
134
+ {
135
+ message: MessagesItem;
136
+ showAvatar?: boolean;
137
+ } & MessageCommonProps
138
+ > = ({ message, showAvatar, ...rest }) => {
139
+ return (
140
+ <View className="flex gap-3 animate-in fade-in-0 slide-in-from-bottom-2 duration-300 overflow-hidden w-full chat-assistant-message">
141
+ <View
142
+ className={[
143
+ "flex-shrink-0 h-7 w-7 rounded-full bg-gradient-to-br from-primary to-primary/20 flex items-center justify-center text-left",
144
+ !showAvatar && "invisible",
145
+ ].join(" ")}
146
+ >
147
+ {showAvatar && (
148
+ <View className="i-lucide-message-square h-3.5 w-3.5 text-primary-foreground" />
149
+ )}
150
+ </View>
151
+ <MessageContentRenderer message={message} {...rest} />
152
+ </View>
153
+ );
154
+ };
@@ -0,0 +1,27 @@
1
+ import { Text, View } from "@tarojs/components";
2
+ import type React from "react";
3
+ import { useSafeArea } from "../hooks/useSafeArea";
4
+ import { useAiAssistantI18n } from "../i18n";
5
+
6
+ export const ChatHeader: React.FC = () => {
7
+ const { t } = useAiAssistantI18n();
8
+ const safeAreaInsets = useSafeArea();
9
+
10
+ return (
11
+ <View
12
+ className="flex items-center px-4 py-3 border-b border-primary/40 bg-background/80 backdrop-blur-sm"
13
+ style={{ paddingTop: `${12 + safeAreaInsets.top}px` }}
14
+ >
15
+ <View className="flex items-center gap-2.5">
16
+ <View className="flex items-center justify-center h-9 w-9 rounded-full bg-gradient-to-br from-primary to-primary/80 shadow-sm">
17
+ <View className="i-lucide-bot text-primary-foreground text-lg" />
18
+ </View>
19
+ <View className="flex flex-col">
20
+ <Text className="text-lg font-semibold text-foreground">
21
+ {t.title}
22
+ </Text>
23
+ </View>
24
+ </View>
25
+ </View>
26
+ );
27
+ };
@@ -0,0 +1,204 @@
1
+ import { Text, Textarea, View } from "@tarojs/components";
2
+ import type React from "react";
3
+ import { useMemo, useState } from "react";
4
+ import { useSafeArea } from "../hooks/useSafeArea";
5
+ import { useAiAssistantI18n } from "../i18n";
6
+ import type { Conversation } from "../types";
7
+ import { VoiceInput } from "./VoiceInput";
8
+
9
+ interface ChatInputProps {
10
+ conversations: Conversation[];
11
+ isLoading: boolean;
12
+ starting?: boolean;
13
+ inputValue: string;
14
+ onInputChange: (value: string) => void;
15
+ onInputAppend?: (value: string) => void;
16
+ onSendMessage: () => void;
17
+ onNewConversation?: () => void;
18
+ onCancel: () => void;
19
+ recommendedQuestions?: string[];
20
+ onQuestionClick?: (question: string) => void;
21
+ }
22
+
23
+ const SubmitButton: React.FC<{
24
+ disabled: boolean;
25
+ starting?: boolean;
26
+ onClick: () => void;
27
+ }> = ({ disabled, starting, onClick }) => {
28
+ const { t } = useAiAssistantI18n();
29
+ return (
30
+ <View
31
+ onClick={onClick}
32
+ className={`flex items-center justify-center gap-1.5 px-4 py-2 rounded-xl transition-all duration-200 shadow-sm text-primary-foreground bg-gradient-to-r from-primary to-primary/80 active:opacity-80 ${
33
+ disabled ? "opacity-50 active:opacity-50 cursor-not-allowed" : ""
34
+ }`}
35
+ hoverClass={disabled ? "" : "opacity-90"}
36
+ >
37
+ <View className={`i-lucide-send text-sm`} />
38
+ <Text className={`text-sm font-medium`}>
39
+ {starting ? t.loading : t.send}
40
+ </Text>
41
+ </View>
42
+ );
43
+ };
44
+
45
+ const StopButton: React.FC<{ onClick: () => void }> = ({ onClick }) => {
46
+ const { t } = useAiAssistantI18n();
47
+ return (
48
+ <View
49
+ onClick={onClick}
50
+ className="flex items-center justify-center gap-1.5 px-4 py-2 rounded-xl active:opacity-80 transition-all duration-200 shadow-sm text-primary-foreground bg-primary"
51
+ hoverClass="opacity-90"
52
+ >
53
+ <View className="i-lucide-square text-sm" />
54
+ <Text className="text-sm font-medium">{t.stopGenerating}</Text>
55
+ </View>
56
+ );
57
+ };
58
+
59
+ const NewConversationButton: React.FC<{
60
+ disabled: boolean;
61
+ onClick: () => void;
62
+ }> = ({ disabled, onClick }) => {
63
+ const { t } = useAiAssistantI18n();
64
+ return (
65
+ <View className="flex items-center gap-2">
66
+ <View
67
+ onClick={disabled ? undefined : onClick}
68
+ className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg transition-all duration-200 ${
69
+ disabled ? "opacity-40" : "active:bg-primary/40"
70
+ }`}
71
+ hoverClass={disabled ? "" : "bg-primary/20"}
72
+ >
73
+ <View className="i-lucide-message-circle-plus text-primary text-base" />
74
+ <Text className="text-sm text-primary">{t.newConversation}</Text>
75
+ </View>
76
+ </View>
77
+ );
78
+ };
79
+
80
+ export const ChatInput: React.FC<ChatInputProps> = ({
81
+ conversations,
82
+ isLoading,
83
+ starting,
84
+ inputValue,
85
+ onInputChange,
86
+ onInputAppend,
87
+ onSendMessage,
88
+ onNewConversation,
89
+ onCancel,
90
+ recommendedQuestions = [],
91
+ onQuestionClick,
92
+ }) => {
93
+ const safeAreaInsets = useSafeArea();
94
+ const { t } = useAiAssistantI18n();
95
+ const [isFocused, setIsFocused] = useState(false);
96
+
97
+ const hasConversations = conversations.length > 0;
98
+ const lastConv =
99
+ conversations.length > 0 ? conversations[conversations.length - 1] : null;
100
+ const lastIsDivider = useMemo(() => {
101
+ if (!lastConv) return false;
102
+ return lastConv.system?.level === "newConversation";
103
+ }, [lastConv]);
104
+ const disabledNewConversation =
105
+ !hasConversations || lastIsDivider || starting || isLoading;
106
+
107
+ const handleInput = (e: { detail: { value: string } }) => {
108
+ onInputChange(e.detail.value);
109
+ };
110
+
111
+ const handleConfirm = () => {
112
+ if (!isLoading && inputValue.trim()) {
113
+ onSendMessage();
114
+ }
115
+ };
116
+
117
+ const handleVoiceResult = (text: string) => {
118
+ if (text) {
119
+ if (onInputAppend) {
120
+ onInputAppend(text);
121
+ } else {
122
+ onInputChange(inputValue + text);
123
+ }
124
+ }
125
+ };
126
+
127
+ return (
128
+ <View
129
+ className="px-4 py-3 border-t border-primary/40 bg-background"
130
+ style={{ paddingBottom: `${8 + safeAreaInsets.bottom}px` }}
131
+ >
132
+ {!isLoading && recommendedQuestions.length > 0 && (
133
+ <View className="mb-3">
134
+ <View className="flex flex-wrap gap-2">
135
+ {recommendedQuestions.map((question) => (
136
+ <View
137
+ key={question}
138
+ onClick={() => onQuestionClick?.(question)}
139
+ className="text-xs px-3 py-1.5 bg-primary/20 text-primary rounded-full border border-primary/40 active:bg-primary/30"
140
+ hoverClass="bg-primary/40"
141
+ >
142
+ <Text>{question}</Text>
143
+ </View>
144
+ ))}
145
+ </View>
146
+ </View>
147
+ )}
148
+
149
+ <View
150
+ className={`w-full rounded-xl bg-primary/10 transition-all duration-200 p-3 border ${
151
+ isFocused ? "border-primary shadow-sm" : "border-primary/40"
152
+ }`}
153
+ data-role="chat-input"
154
+ >
155
+ <Textarea
156
+ value={inputValue}
157
+ onInput={handleInput}
158
+ onFocus={() => setIsFocused(true)}
159
+ onBlur={() => setIsFocused(false)}
160
+ placeholder={t.inputPlaceholder}
161
+ disabled={isLoading}
162
+ maxlength={2000}
163
+ autoHeight
164
+ fixed
165
+ style={{ minHeight: "48px", maxHeight: "120px" }}
166
+ className="w-full text-base text-foreground placeholder:text-muted-foreground leading-relaxed disabled:opacity-50"
167
+ data-role="chat-textarea"
168
+ />
169
+
170
+ <View
171
+ className="flex items-center justify-between mt-3 pt-2"
172
+ data-role="chat-tools"
173
+ >
174
+ <View className="flex items-center gap-2">
175
+ <NewConversationButton
176
+ disabled={disabledNewConversation}
177
+ onClick={onNewConversation!}
178
+ />
179
+ </View>
180
+
181
+ <View className="flex items-center gap-2">
182
+ <VoiceInput
183
+ onResult={handleVoiceResult}
184
+ disabled={starting}
185
+ />
186
+ {isLoading ? (
187
+ <StopButton onClick={onCancel} />
188
+ ) : (
189
+ <SubmitButton
190
+ disabled={starting || isLoading || !inputValue.trim()}
191
+ starting={starting}
192
+ onClick={handleConfirm}
193
+ />
194
+ )}
195
+ </View>
196
+ </View>
197
+ </View>
198
+
199
+ <View className="text-[10px] text-muted mt-2 text-center">
200
+ {t.sendHint}
201
+ </View>
202
+ </View>
203
+ );
204
+ };
@@ -0,0 +1,126 @@
1
+ import { ScrollView, Text, View } from "@tarojs/components";
2
+ import type React from "react";
3
+ import { useCallback, useEffect, useState } from "react";
4
+ import { useAiAssistantI18n } from "../i18n";
5
+ import type { Conversation } from "../types";
6
+ import { ChatAssistantMessage, ChatLoading } from "./ChatAssistantMessage";
7
+ import { ChatUserMessage } from "./ChatUserMessage";
8
+
9
+ interface ChatMessagesProps {
10
+ conversations: Conversation[];
11
+ isLoading: boolean;
12
+ isLoadingHistory?: boolean;
13
+ hasMoreHistory?: boolean;
14
+ onLoadMore?: () => void;
15
+ }
16
+
17
+ export const ChatMessages: React.FC<ChatMessagesProps> = ({
18
+ conversations,
19
+ isLoading,
20
+ isLoadingHistory,
21
+ hasMoreHistory,
22
+ onLoadMore,
23
+ }) => {
24
+ const [scrollTop, setScrollTop] = useState(0);
25
+ const { t } = useAiAssistantI18n();
26
+
27
+ const scrollToBottom = useCallback(() => {
28
+ setScrollTop((prev) => prev + 10000);
29
+ }, []);
30
+
31
+ useEffect(() => {
32
+ scrollToBottom();
33
+ }, [conversations, scrollToBottom]);
34
+
35
+ const handleScrollToUpper = useCallback(() => {
36
+ if (onLoadMore && hasMoreHistory && !isLoadingHistory) {
37
+ onLoadMore();
38
+ }
39
+ }, [onLoadMore, hasMoreHistory, isLoadingHistory]);
40
+
41
+ return (
42
+ <ScrollView
43
+ scrollY
44
+ scrollTop={scrollTop}
45
+ scrollWithAnimation
46
+ className="flex-1 overflow-hidden"
47
+ onScrollToLower={scrollToBottom}
48
+ onScrollToUpper={handleScrollToUpper}
49
+ upperThreshold={50}
50
+ >
51
+ <View className="flex flex-col min-h-full">
52
+ {isLoadingHistory && (
53
+ <View className="flex justify-center items-center py-4">
54
+ <Text className="text-sm text-gray-400">{t.loadingHistory}</Text>
55
+ </View>
56
+ )}
57
+ {!isLoadingHistory && hasMoreHistory && (
58
+ <View className="flex justify-center items-center py-4">
59
+ <Text className="text-sm text-gray-400">{t.loadMore}</Text>
60
+ </View>
61
+ )}
62
+ {conversations.map((conversation, index) => {
63
+ const len = conversation.messages.length;
64
+ const historyId = conversation.historyId || "";
65
+ const lastHistoryId = conversations[index - 1]?.historyId || "";
66
+ const showDivider =
67
+ index > 0 &&
68
+ (historyId !== lastHistoryId ||
69
+ conversation.system?.level === "newConversation");
70
+
71
+ return (
72
+ <View key={conversation.taskId} className="flex flex-col">
73
+ {showDivider && (
74
+ <View className="flex items-center justify-center py-4">
75
+ <View className="flex-1 h-px bg-gray-200" />
76
+ <Text className="px-4 text-xs text-gray-400">
77
+ {t.conversationDivider}
78
+ </Text>
79
+ <View className="flex-1 h-px bg-gray-200" />
80
+ </View>
81
+ )}
82
+ <View className="flex flex-col gap-2 px-4 pt-4">
83
+ {conversation.messages.map((message, msgIndex) => {
84
+ const key = message.messageId || `${index}-${msgIndex}`;
85
+ const isNewest = msgIndex === len - 1;
86
+ if (message.role === "assistant") {
87
+ return (
88
+ <ChatAssistantMessage
89
+ key={key}
90
+ message={message}
91
+ isNewest={isNewest}
92
+ isLoading={isLoading}
93
+ showAvatar={
94
+ msgIndex === 0 ||
95
+ conversation.messages[msgIndex - 1].role !==
96
+ "assistant"
97
+ }
98
+ />
99
+ );
100
+ }
101
+ return <ChatUserMessage key={key} message={message} />;
102
+ })}
103
+ </View>
104
+ </View>
105
+ );
106
+ })}
107
+ {isLoading && (
108
+ <View key="loading" className="flex flex-col gap-2 px-4 pt-2">
109
+ <ChatAssistantMessage
110
+ key="loading"
111
+ message={{
112
+ kind: "text-content",
113
+ messageId: "loading",
114
+ role: "assistant",
115
+ content: "",
116
+ }}
117
+ isLoading={true}
118
+ showAvatar={false}
119
+ />
120
+ </View>
121
+ )}
122
+ <View className="w-full mt-auto sticky bottom-0 flex items-center justify-center py-2" />
123
+ </View>
124
+ </ScrollView>
125
+ );
126
+ };
@@ -0,0 +1,25 @@
1
+ import { View } from "@tarojs/components";
2
+ import type { MessagesItem, TextMessage } from "../types";
3
+ import Markdown from "./markdown";
4
+
5
+ export const ChatUserMessage: React.FC<{
6
+ message: MessagesItem;
7
+ }> = ({ message }) => {
8
+ return (
9
+ <View
10
+ className={`flex gap-3 animate-in fade-in-0 slide-in-from-bottom-2 duration-300 overflow-hidden w-full flex-row-reverse`}
11
+ >
12
+ <View
13
+ className={`
14
+ px-4 py-2.5 rounded-2xl
15
+ transition-all duration-200 min-w-0 overflow-hidden max-w-full
16
+ bg-primary/20 text-foreground rounded-br-md
17
+ `}
18
+ >
19
+ <View className="text-lg leading-relaxed whitespace-pre-wrap break-words">
20
+ <Markdown content={(message as TextMessage).content || "..."} />
21
+ </View>
22
+ </View>
23
+ </View>
24
+ );
25
+ };