@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.
- package/README.md +12 -8
- package/components/ai-assistant/amaster.config.json +3 -0
- package/components/ai-assistant/package.json +3 -3
- package/components/ai-assistant/template/components/chat-assistant-message.tsx +1 -1
- package/components/ai-assistant/template/components/chat-banner.tsx +1 -1
- package/components/ai-assistant/template/components/chat-display-mode-switcher.tsx +6 -6
- package/components/ai-assistant/template/components/chat-floating-button.tsx +4 -5
- package/components/ai-assistant/template/components/chat-floating-card.tsx +3 -3
- package/components/ai-assistant/template/components/chat-header.tsx +5 -5
- package/components/ai-assistant/template/components/chat-input.tsx +21 -22
- package/components/ai-assistant/template/components/chat-messages.tsx +10 -2
- package/components/ai-assistant/template/components/chat-recommends.tsx +12 -14
- package/components/ai-assistant/template/components/chat-user-message.tsx +1 -1
- package/components/ai-assistant/template/components/ui-renderer.tsx +1 -1
- package/components/ai-assistant/template/components/voice-input.tsx +1 -1
- package/components/ai-assistant/template/hooks/useVoiceInput.ts +18 -20
- package/components/ai-assistant/template/inline-ai-assistant.tsx +1 -1
- package/components/ai-assistant-taro/amaster.config.json +3 -0
- package/components/ai-assistant-taro/package.json +94 -0
- package/components/ai-assistant-taro/template/components/ChatAssistantMessage.tsx +154 -0
- package/components/ai-assistant-taro/template/components/ChatHeader.tsx +27 -0
- package/components/ai-assistant-taro/template/components/ChatInput.tsx +204 -0
- package/components/ai-assistant-taro/template/components/ChatMessages.tsx +126 -0
- package/components/ai-assistant-taro/template/components/ChatUserMessage.tsx +25 -0
- package/components/ai-assistant-taro/template/components/VoiceInput.tsx +169 -0
- package/components/ai-assistant-taro/template/components/markdown.tsx +156 -0
- package/components/ai-assistant-taro/template/hooks/useConversation.ts +787 -0
- package/components/ai-assistant-taro/template/hooks/useSafeArea.ts +20 -0
- package/components/ai-assistant-taro/template/hooks/useVoiceInput.ts +204 -0
- package/components/ai-assistant-taro/template/i18n.ts +157 -0
- package/components/ai-assistant-taro/template/index.config.ts +10 -0
- package/components/ai-assistant-taro/template/index.tsx +83 -0
- package/components/ai-assistant-taro/template/types.ts +58 -0
- package/package.json +5 -2
- package/packages/cli/dist/index.js +14 -3
- package/packages/cli/dist/index.js.map +1 -1
- package/packages/cli/package.json +1 -1
- package/components/ai-assistant/example.md +0 -34
- 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
|
+
};
|