@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,94 @@
1
+ {
2
+ "name": "taro-project",
3
+ "version": "1.8.0",
4
+ "description": "开箱即用的基于Taro + React + Zustand + TailwindCSS + TypeScript的模板",
5
+ "author": "amaster.ai",
6
+ "license": "MIT",
7
+ "templateInfo": {
8
+ "name": "default",
9
+ "typescript": true,
10
+ "css": "sass"
11
+ },
12
+ "scripts": {
13
+ "dev": "bun run build:h5 -- --watch",
14
+ "build:weapp": "taro build --type weapp",
15
+ "build:swan": "taro build --type swan",
16
+ "build:alipay": "taro build --type alipay",
17
+ "build:tt": "taro build --type tt",
18
+ "build:h5": "taro build --type h5",
19
+ "build:rn": "taro build --type rn",
20
+ "build:qq": "taro build --type qq",
21
+ "build:jd": "taro build --type jd",
22
+ "build:quickapp": "taro build --type quickapp",
23
+ "lint": "biome check --write .",
24
+ "lint:check": "biome check .",
25
+ "lint:fix": "biome check --write --unsafe .",
26
+ "lint:format": "prettier --write 'src/**/*.{ts,tsx,js,jsx,json,css,scss,md}'",
27
+ "type-check": "tsc --noEmit",
28
+ "check:build": "node scripts/check-build.mjs",
29
+ "precommit": "bun run lint:check && bun run type-check",
30
+ "prepare": "husky || true",
31
+ "postinstall": "weapp-tw patch || true",
32
+ "patch": "weapp-tw patch"
33
+ },
34
+ "browserslist": [
35
+ "last 5 years",
36
+ "Android >= 9",
37
+ "iOS >= 13",
38
+ "not dead"
39
+ ],
40
+ "dependencies": {
41
+ "@a2a-js/sdk": "^0.3.7",
42
+ "@amaster.ai/bpm-ui": "1.1.0-beta.61",
43
+ "@amaster.ai/client": "1.1.0-beta.61",
44
+ "@amaster.ai/taro-echarts-ui": "1.1.0-beta.61",
45
+ "@amaster.ai/vite-plugins": "1.1.0-beta.61",
46
+ "@babel/runtime": "^7.28.3",
47
+ "@tarojs/components": "4.1.5",
48
+ "@tarojs/helper": "4.1.5",
49
+ "@tarojs/plugin-framework-react": "4.1.5",
50
+ "@tarojs/plugin-generator": "4.1.5",
51
+ "@tarojs/plugin-html": "4.1.5",
52
+ "@tarojs/plugin-http": "4.1.5",
53
+ "@tarojs/plugin-platform-h5": "4.1.5",
54
+ "@tarojs/plugin-platform-weapp": "4.1.5",
55
+ "@tarojs/react": "4.1.5",
56
+ "@tarojs/runtime": "4.1.5",
57
+ "@tarojs/shared": "4.1.5",
58
+ "@tarojs/taro": "4.1.5",
59
+ "immer": "^10.1.1",
60
+ "markdown-it": "^14.1.1",
61
+ "react": "^18.3.1",
62
+ "react-dom": "^18.3.1",
63
+ "zustand": "^5.0.8"
64
+ },
65
+ "devDependencies": {
66
+ "@babel/core": "^7.28.3",
67
+ "@babel/preset-react": "^7.24.1",
68
+ "@biomejs/biome": "^2.2.3",
69
+ "@egoist/tailwindcss-icons": "^1.9.0",
70
+ "@iconify-json/lucide": "^1.2.64",
71
+ "@iconify-json/mdi": "^1.2.3",
72
+ "@tarojs/cli": "4.1.5",
73
+ "@tarojs/taro-loader": "4.1.5",
74
+ "@tarojs/vite-runner": "4.1.5",
75
+ "@types/node": "^24.3.0",
76
+ "@types/react": "^18.3.24",
77
+ "@typescript/native-preview": "7.0.0-dev.20250827.1",
78
+ "@vitejs/plugin-react": "^4.7.0",
79
+ "babel-preset-taro": "4.1.5",
80
+ "husky": "^9.1.7",
81
+ "postcss": "^8.5.6",
82
+ "prettier": "^3.8.1",
83
+ "react-refresh": "^0.14.0",
84
+ "tailwindcss": "^3.4.17",
85
+ "ts-node": "^10.9.2",
86
+ "typescript": "^5.9.2",
87
+ "vite": "^4.5.14",
88
+ "weapp-tailwindcss": "^4.2.6"
89
+ },
90
+ "optionalDependencies": {
91
+ "@tarojs/binding-linux-arm64-gnu": "^4.1.10",
92
+ "@tarojs/binding-linux-x64-gnu": "4.1.5"
93
+ }
94
+ }
@@ -0,0 +1,176 @@
1
+ import { View } from "@tarojs/components";
2
+ import { useState } from "react";
3
+ import { JsonRenderer } from "@/components/json-render";
4
+ import { useAiAssistantI18n } from "../i18n";
5
+ import type {
6
+ MessagesItem,
7
+ TextMessage,
8
+ ThoughtMessage,
9
+ ToolMessage,
10
+ UIRenderMessage,
11
+ } from "../types";
12
+ import Markdown from "./markdown";
13
+
14
+ interface MessageCommonProps {
15
+ isNewest?: boolean;
16
+ isLoading?: boolean;
17
+ }
18
+
19
+ export const ChatLoading: React.FC<
20
+ { className?: string } & MessageCommonProps
21
+ > = ({ className }) => {
22
+ return (
23
+ <View className={`flex items-center gap-1 ${className}`}>
24
+ <View className="h-2 w-2 rounded-full bg-primary animate-pulse" />
25
+ <View
26
+ className="h-2 w-2 rounded-full bg-primary animate-pulse"
27
+ style={{ animationDelay: "150ms" }}
28
+ />
29
+ <View
30
+ className="h-2 w-2 rounded-full bg-primary animate-pulse"
31
+ style={{ animationDelay: "300ms" }}
32
+ />
33
+ </View>
34
+ );
35
+ };
36
+
37
+ const ChatTextMessage: React.FC<{ message: TextMessage }> = ({ message }) => {
38
+ if (!message.content) {
39
+ return <ChatLoading className="ml-2" />;
40
+ }
41
+
42
+ return (
43
+ <View className="whitespace-pre-wrap leading-relaxed break-words overflow-hidden min-w-0 max-w-full px-1">
44
+ <Markdown content={message.content} />
45
+ </View>
46
+ );
47
+ };
48
+
49
+ const ChatThoughtMessage: React.FC<
50
+ { message: ThoughtMessage } & MessageCommonProps
51
+ > = ({ message, isNewest, isLoading }) => {
52
+ const [expanded, setExpanded] = useState(false);
53
+ const { t } = useAiAssistantI18n();
54
+ const thinking = isLoading && isNewest;
55
+ return (
56
+ <View className="leading-relaxed whitespace-pre-wrap break-words text-sm overflow-hidden text-left">
57
+ <View
58
+ 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"
59
+ onClick={() => setExpanded(!expanded)}
60
+ >
61
+ {thinking ? (
62
+ <View className="i-lucide-loader-circle text-blue-600 animate-spin size-4" />
63
+ ) : !expanded ? (
64
+ <View className="i-lucide-chevron-down size-4" />
65
+ ) : (
66
+ <View className="i-lucide-chevron-up size-4" />
67
+ )}
68
+ <View>{thinking ? t.thinking : t.thinkResult}</View>
69
+ </View>
70
+ {(expanded || thinking) && (
71
+ <Markdown
72
+ content={message.thought || "..."}
73
+ className="font-normal opacity-55 pl-4 py-2 border-l-[2px] ml-4"
74
+ />
75
+ )}
76
+ </View>
77
+ );
78
+ };
79
+
80
+ const ChatToolMessage: React.FC<
81
+ { message: ToolMessage } & MessageCommonProps
82
+ > = ({ message, isLoading }) => {
83
+ const status = message.toolStatus || "executing";
84
+ const { t } = useAiAssistantI18n();
85
+ return (
86
+ <View className="leading-relaxed whitespace-pre-wrap break-words flex items-center gap-1 border px-2 p-1 rounded-xl bg-primary/50 text-primary-foreground text-sm max-w-full overflow-hidden">
87
+ {status === "success" ? (
88
+ <View className="i-lucide-badge-check size-4 text-green-600 fill-current shrink-0" />
89
+ ) : status === "failed" || status === "error" ? (
90
+ <View className="i-lucide-info size-4 text-red-600 shrink-0" />
91
+ ) : isLoading ? (
92
+ <View className="i-lucide-refresh-cw size-4 text-primary animate-spin shrink-0 text-warning" />
93
+ ) : (
94
+ <View className="i-lucide-circle-x size-4 text-orange-600 shrink-0" />
95
+ )}
96
+ <View className="flex-1 truncate flex-shrink-0">
97
+ {message.toolName || t.unknownTool}
98
+ </View>
99
+ </View>
100
+ );
101
+ };
102
+
103
+ const ChatErrorMessage: React.FC<
104
+ { message: TextMessage } & MessageCommonProps
105
+ > = ({ message }) => {
106
+ const { t } = useAiAssistantI18n();
107
+ return (
108
+ <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">
109
+ {message.content || t.errorMessage}
110
+ </View>
111
+ );
112
+ };
113
+
114
+ const ChatUIRenderMessage: React.FC<{ message: UIRenderMessage }> = ({
115
+ message,
116
+ }) => {
117
+ if (!message.spec) {
118
+ return (
119
+ <View className="p-2 border border-destructive rounded-lg bg-destructive/10">
120
+ <View className="text-destructive">Invalid UI spec</View>
121
+ </View>
122
+ );
123
+ }
124
+
125
+ return (
126
+ <View className="w-full overflow-hidden">
127
+ <JsonRenderer spec={message.spec} />
128
+ </View>
129
+ );
130
+ };
131
+
132
+ const MessageContentRenderer: React.FC<
133
+ {
134
+ message: MessagesItem;
135
+ } & MessageCommonProps
136
+ > = ({ message, ...rest }) => {
137
+ switch (message.kind) {
138
+ case "text-content":
139
+ return <ChatTextMessage message={message as TextMessage} {...rest} />;
140
+ case "thought":
141
+ return (
142
+ <ChatThoughtMessage message={message as ThoughtMessage} {...rest} />
143
+ );
144
+ case "tool":
145
+ return <ChatToolMessage message={message as ToolMessage} {...rest} />;
146
+ case "error":
147
+ return <ChatErrorMessage message={message as TextMessage} {...rest} />;
148
+ case "ui-render":
149
+ return <ChatUIRenderMessage message={message as UIRenderMessage} />;
150
+ default:
151
+ return null;
152
+ }
153
+ };
154
+
155
+ export const ChatAssistantMessage: React.FC<
156
+ {
157
+ message: MessagesItem;
158
+ showAvatar?: boolean;
159
+ } & MessageCommonProps
160
+ > = ({ message, showAvatar, ...rest }) => {
161
+ return (
162
+ <View className="flex gap-3 animate-in fade-in-0 slide-in-from-bottom-2 duration-300 overflow-hidden w-full chat-assistant-message">
163
+ <View
164
+ className={[
165
+ "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",
166
+ !showAvatar && "invisible",
167
+ ].join(" ")}
168
+ >
169
+ {showAvatar && (
170
+ <View className="i-lucide-message-square h-3.5 w-3.5 text-primary-foreground" />
171
+ )}
172
+ </View>
173
+ <MessageContentRenderer message={message} {...rest} />
174
+ </View>
175
+ );
176
+ };
@@ -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,233 @@
1
+ import { Text, Textarea, View } from "@tarojs/components";
2
+ import type React from "react";
3
+ import { useMemo, useRef, 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
+ const textareaRef = useRef<any>(null);
107
+
108
+ const handleInput = (e: { detail: { value: string } }) => {
109
+ onInputChange(e.detail.value);
110
+ };
111
+
112
+ const handleConfirm = () => {
113
+ if (!isLoading && inputValue.trim()) {
114
+ onSendMessage();
115
+ }
116
+ };
117
+
118
+ const handleVoiceResult = (text: string) => {
119
+ if (text) {
120
+ if (onInputAppend) {
121
+ onInputAppend(text);
122
+ } else {
123
+ onInputChange(inputValue + text);
124
+ }
125
+
126
+ // 2. 延迟执行滚动(等渲染完成)
127
+ setTimeout(() => {
128
+ scrollToEndQuickly();
129
+ }, 60); // 50~100ms,根据真机测试微调
130
+ }
131
+ };
132
+
133
+ const scrollToEndQuickly = () => {
134
+ const ta = textareaRef.current;
135
+ if (!ta) return;
136
+
137
+ const len = inputValue.length;
138
+
139
+ ta.focus?.();
140
+ ta.setSelectionRange?.(len, len);
141
+
142
+ setTimeout(() => {
143
+ ta.blur?.();
144
+ }, 30);
145
+ };
146
+
147
+ return (
148
+ <View
149
+ className="px-4 py-3"
150
+ style={{ paddingBottom: `${8 + safeAreaInsets.bottom}px` }}
151
+ >
152
+ {!isLoading && recommendedQuestions.length > 0 && (
153
+ <View className="mb-3">
154
+ <View className="flex flex-wrap gap-2">
155
+ {recommendedQuestions.map((question) => (
156
+ <View
157
+ key={question}
158
+ onClick={() => onQuestionClick?.(question)}
159
+ className="text-xs px-3 py-1.5 bg-primary/20 text-primary rounded-full border border-primary/40 active:bg-primary/30"
160
+ hoverClass="bg-primary/40"
161
+ >
162
+ <Text>{question}</Text>
163
+ </View>
164
+ ))}
165
+ </View>
166
+ </View>
167
+ )}
168
+
169
+ <View
170
+ className={`w-full rounded-xl bg-background transition-all duration-200 p-3 border ${
171
+ isFocused ? "border-primary shadow-sm" : "border-primary/40"
172
+ }`}
173
+ data-role="chat-input"
174
+ >
175
+ <Textarea
176
+ ref={textareaRef}
177
+ value={inputValue}
178
+ onInput={handleInput}
179
+ onFocus={() => setIsFocused(true)}
180
+ onBlur={() => setIsFocused(false)}
181
+ placeholder={t.inputPlaceholder}
182
+ disabled={isLoading}
183
+ maxlength={10000}
184
+ className="w-full text-base text-foreground placeholder:text-muted-foreground leading-relaxed disabled:opacity-50 resize-none"
185
+ data-role="chat-textarea"
186
+ confirm-type="send"
187
+ style={{
188
+ height: "60px",
189
+ minHeight: "60px",
190
+ maxHeight: "60px",
191
+ resize: "none",
192
+ background: "none",
193
+ fontSize: "14px",
194
+ }}
195
+ >
196
+ {/* 不要删掉,这是为了解决 h5 textarea 样式问题 */}
197
+ <style>
198
+ {`.taro-textarea {background: none;resize: none;}`}
199
+ </style>
200
+ </Textarea>
201
+
202
+ <View
203
+ className="flex items-center justify-between mt-1 pt-2"
204
+ data-role="chat-tools"
205
+ >
206
+ <View className="flex items-center gap-2">
207
+ <NewConversationButton
208
+ disabled={disabledNewConversation}
209
+ onClick={onNewConversation}
210
+ />
211
+ </View>
212
+
213
+ <View className="flex items-center gap-2">
214
+ <VoiceInput onResult={handleVoiceResult} disabled={starting} />
215
+ {isLoading ? (
216
+ <StopButton onClick={onCancel} />
217
+ ) : (
218
+ <SubmitButton
219
+ disabled={starting || isLoading || !inputValue.trim()}
220
+ starting={starting}
221
+ onClick={handleConfirm}
222
+ />
223
+ )}
224
+ </View>
225
+ </View>
226
+ </View>
227
+
228
+ <View className="text-[10px] text-muted mt-2 text-center">
229
+ {t.sendHint}
230
+ </View>
231
+ </View>
232
+ );
233
+ };
@@ -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
+ };