@amaster.ai/components-templates 1.6.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.
- package/components/ai-assistant/package.json +3 -3
- package/components/ai-assistant/template/components/chat-input.tsx +17 -4
- package/components/ai-assistant/template/components/chat-messages.tsx +1 -1
- package/components/ai-assistant/template/components/voice-input.tsx +9 -1
- package/components/ai-assistant/template/hooks/useConversation.ts +0 -23
- package/components/ai-assistant-taro/package.json +5 -5
- package/components/ai-assistant-taro/template/components/ChatAssistantMessage.tsx +24 -2
- package/components/ai-assistant-taro/template/components/ChatInput.tsx +45 -16
- package/components/ai-assistant-taro/template/components/markdown.tsx +343 -137
- package/components/ai-assistant-taro/template/hooks/useConversation.ts +542 -424
- package/components/ai-assistant-taro/template/index.tsx +2 -2
- package/components/ai-assistant-taro/template/types.ts +16 -0
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "amaster-react-project",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"dev": "vite --force",
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
19
|
"@a2a-js/sdk": "^0.3.7",
|
|
20
|
-
"@amaster.ai/client": "1.1.0-beta.
|
|
21
|
-
"@amaster.ai/vite-plugins": "1.1.0-beta.
|
|
20
|
+
"@amaster.ai/client": "1.1.0-beta.61",
|
|
21
|
+
"@amaster.ai/vite-plugins": "1.1.0-beta.61",
|
|
22
22
|
"@fontsource-variable/inter": "^5.2.8",
|
|
23
23
|
"@fortawesome/fontawesome-free": "^6.1.1",
|
|
24
24
|
"@hookform/resolvers": "^5.2.2",
|
|
@@ -9,7 +9,7 @@ import type React from "react";
|
|
|
9
9
|
import { Button } from "@/components/ui/button";
|
|
10
10
|
import { getText } from "../i18n";
|
|
11
11
|
import type { Conversation, IDisplayMode } from "../types";
|
|
12
|
-
import { useMemo, useState } from "react";
|
|
12
|
+
import { useMemo, useRef, useState } from "react";
|
|
13
13
|
import { cn } from "@/lib/utils";
|
|
14
14
|
import {
|
|
15
15
|
Tooltip,
|
|
@@ -118,6 +118,8 @@ const ChatInput: React.FC<ChatInputProps> = ({
|
|
|
118
118
|
if (!lastConv) return false;
|
|
119
119
|
return lastConv.system?.level === "newConversation";
|
|
120
120
|
}, [lastConv]);
|
|
121
|
+
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
122
|
+
const [speaking, setSpeaking] = useState(false);
|
|
121
123
|
|
|
122
124
|
const [focus, setFocus] = useState(false);
|
|
123
125
|
const handleKeyPress = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
|
@@ -127,6 +129,12 @@ const ChatInput: React.FC<ChatInputProps> = ({
|
|
|
127
129
|
}
|
|
128
130
|
};
|
|
129
131
|
|
|
132
|
+
const textareaScrollToBottom = () => {
|
|
133
|
+
if (textareaRef.current) {
|
|
134
|
+
textareaRef.current.scrollTop = textareaRef.current.scrollHeight;
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
130
138
|
return (
|
|
131
139
|
<div className="py-3 px-4">
|
|
132
140
|
<div
|
|
@@ -138,16 +146,19 @@ const ChatInput: React.FC<ChatInputProps> = ({
|
|
|
138
146
|
)}
|
|
139
147
|
>
|
|
140
148
|
<textarea
|
|
149
|
+
ref={textareaRef}
|
|
141
150
|
value={inputValue}
|
|
142
151
|
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
|
|
143
152
|
onInputChange(e.target.value)
|
|
144
153
|
}
|
|
145
|
-
onKeyPress={isLoading ? undefined : handleKeyPress}
|
|
154
|
+
onKeyPress={isLoading || speaking ? undefined : handleKeyPress}
|
|
146
155
|
placeholder={getText().typePlaceholder}
|
|
147
156
|
onFocus={() => setFocus(true)}
|
|
148
157
|
onBlur={() => setFocus(false)}
|
|
149
158
|
className="w-full text-sm text-foreground placeholder:text-muted-foreground outline-none resize-none leading-5 bg-card"
|
|
150
159
|
rows={4}
|
|
160
|
+
disabled={speaking}
|
|
161
|
+
readOnly={speaking}
|
|
151
162
|
/>
|
|
152
163
|
<div className="flex items-center justify-between gap-2 mt-1">
|
|
153
164
|
<div className="flex items-center gap-1">
|
|
@@ -164,17 +175,19 @@ const ChatInput: React.FC<ChatInputProps> = ({
|
|
|
164
175
|
<VoiceInputButton
|
|
165
176
|
onChange={(text) => {
|
|
166
177
|
onInputChange(text);
|
|
178
|
+
textareaScrollToBottom();
|
|
167
179
|
setFocus(true);
|
|
168
180
|
}}
|
|
169
181
|
value={inputValue}
|
|
170
182
|
disabled={isLoading || starting}
|
|
183
|
+
onRunningChange={setSpeaking}
|
|
171
184
|
/>
|
|
172
|
-
{
|
|
185
|
+
{isLoading ? (
|
|
173
186
|
<StopButton onClick={onCancel || (() => {})} />
|
|
174
187
|
) : (
|
|
175
188
|
<SubmitButton
|
|
176
189
|
onClick={() => onSendMessage()}
|
|
177
|
-
disabled={!inputValue.trim() || isLoading}
|
|
190
|
+
disabled={!inputValue.trim() || isLoading || speaking}
|
|
178
191
|
starting={starting}
|
|
179
192
|
/>
|
|
180
193
|
)}
|
|
@@ -3,12 +3,14 @@ import { Mic, StopCircle } from "lucide-react";
|
|
|
3
3
|
import { Button } from "@/components/ui/button";
|
|
4
4
|
import { cn } from "@/lib/utils";
|
|
5
5
|
import { useVoiceInput } from "../hooks/useVoiceInput";
|
|
6
|
+
import { useEffect } from "react";
|
|
6
7
|
|
|
7
8
|
const VoiceInputButton: React.FC<{
|
|
8
9
|
onChange: (text: string) => void;
|
|
9
10
|
disabled?: boolean;
|
|
10
11
|
value?: string;
|
|
11
|
-
|
|
12
|
+
onRunningChange?: (running: boolean) => void;
|
|
13
|
+
}> = ({ onChange, disabled, value, onRunningChange }) => {
|
|
12
14
|
const {
|
|
13
15
|
start,
|
|
14
16
|
stop,
|
|
@@ -23,6 +25,12 @@ const VoiceInputButton: React.FC<{
|
|
|
23
25
|
value,
|
|
24
26
|
});
|
|
25
27
|
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (onRunningChange) {
|
|
30
|
+
onRunningChange(running);
|
|
31
|
+
}
|
|
32
|
+
}, [running, onRunningChange]);
|
|
33
|
+
|
|
26
34
|
return (
|
|
27
35
|
<Button
|
|
28
36
|
variant="ghost"
|
|
@@ -391,7 +391,6 @@ export function useConversation() {
|
|
|
391
391
|
try {
|
|
392
392
|
const partData = JSON.parse(json);
|
|
393
393
|
if (partData.root && partData.elements) {
|
|
394
|
-
debugger
|
|
395
394
|
fillData({
|
|
396
395
|
kind: 'ui-render',
|
|
397
396
|
taskId,
|
|
@@ -768,28 +767,6 @@ export function useConversation() {
|
|
|
768
767
|
const controller = abortControllerRef.current;
|
|
769
768
|
forceStopRef.current = false;
|
|
770
769
|
|
|
771
|
-
// mock
|
|
772
|
-
if (userContent === "MAGA") {
|
|
773
|
-
const { generateMockUIStream } = await import("../mock/mock-data");
|
|
774
|
-
const cancelMock = generateMockUIStream(
|
|
775
|
-
taskId || generateId(),
|
|
776
|
-
(chunk) => {
|
|
777
|
-
if (forceStopRef.current) return;
|
|
778
|
-
hasResponse = true;
|
|
779
|
-
processLiveData(chunk as any);
|
|
780
|
-
if (chunk.result.final) {
|
|
781
|
-
setIsLoading(false);
|
|
782
|
-
}
|
|
783
|
-
},
|
|
784
|
-
);
|
|
785
|
-
abortControllerRef.current?.signal.addEventListener(
|
|
786
|
-
"abort",
|
|
787
|
-
cancelMock,
|
|
788
|
-
);
|
|
789
|
-
return;
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
// real chat
|
|
793
770
|
try {
|
|
794
771
|
const stream = client.copilot.chat(
|
|
795
772
|
userContent ? [{ role: "user", content: userContent }] : [],
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "taro-project",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"description": "开箱即用的基于Taro + React + Zustand + TailwindCSS + TypeScript的模板",
|
|
5
5
|
"author": "amaster.ai",
|
|
6
6
|
"license": "MIT",
|
|
@@ -39,10 +39,10 @@
|
|
|
39
39
|
],
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"@a2a-js/sdk": "^0.3.7",
|
|
42
|
-
"@amaster.ai/bpm-ui": "1.1.0-beta.
|
|
43
|
-
"@amaster.ai/client": "1.1.0-beta.
|
|
44
|
-
"@amaster.ai/taro-echarts-ui": "1.1.0-beta.
|
|
45
|
-
"@amaster.ai/vite-plugins": "1.1.0-beta.
|
|
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
46
|
"@babel/runtime": "^7.28.3",
|
|
47
47
|
"@tarojs/components": "4.1.5",
|
|
48
48
|
"@tarojs/helper": "4.1.5",
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { View } from "@tarojs/components";
|
|
2
2
|
import { useState } from "react";
|
|
3
|
+
import { JsonRenderer } from "@/components/json-render";
|
|
3
4
|
import { useAiAssistantI18n } from "../i18n";
|
|
4
5
|
import type {
|
|
5
6
|
MessagesItem,
|
|
6
7
|
TextMessage,
|
|
7
8
|
ThoughtMessage,
|
|
8
9
|
ToolMessage,
|
|
10
|
+
UIRenderMessage,
|
|
9
11
|
} from "../types";
|
|
10
12
|
import Markdown from "./markdown";
|
|
11
13
|
|
|
@@ -51,7 +53,7 @@ const ChatThoughtMessage: React.FC<
|
|
|
51
53
|
const { t } = useAiAssistantI18n();
|
|
52
54
|
const thinking = isLoading && isNewest;
|
|
53
55
|
return (
|
|
54
|
-
<View className="leading-relaxed whitespace-pre-wrap break-words text-
|
|
56
|
+
<View className="leading-relaxed whitespace-pre-wrap break-words text-sm overflow-hidden text-left">
|
|
55
57
|
<View
|
|
56
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"
|
|
57
59
|
onClick={() => setExpanded(!expanded)}
|
|
@@ -81,7 +83,7 @@ const ChatToolMessage: React.FC<
|
|
|
81
83
|
const status = message.toolStatus || "executing";
|
|
82
84
|
const { t } = useAiAssistantI18n();
|
|
83
85
|
return (
|
|
84
|
-
<View className="leading-relaxed whitespace-pre-wrap break-words flex items-center gap-1 border px-2 p-1 rounded-xl bg-
|
|
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">
|
|
85
87
|
{status === "success" ? (
|
|
86
88
|
<View className="i-lucide-badge-check size-4 text-green-600 fill-current shrink-0" />
|
|
87
89
|
) : status === "failed" || status === "error" ? (
|
|
@@ -109,6 +111,24 @@ const ChatErrorMessage: React.FC<
|
|
|
109
111
|
);
|
|
110
112
|
};
|
|
111
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
|
+
|
|
112
132
|
const MessageContentRenderer: React.FC<
|
|
113
133
|
{
|
|
114
134
|
message: MessagesItem;
|
|
@@ -125,6 +145,8 @@ const MessageContentRenderer: React.FC<
|
|
|
125
145
|
return <ChatToolMessage message={message as ToolMessage} {...rest} />;
|
|
126
146
|
case "error":
|
|
127
147
|
return <ChatErrorMessage message={message as TextMessage} {...rest} />;
|
|
148
|
+
case "ui-render":
|
|
149
|
+
return <ChatUIRenderMessage message={message as UIRenderMessage} />;
|
|
128
150
|
default:
|
|
129
151
|
return null;
|
|
130
152
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Text, Textarea, View } from "@tarojs/components";
|
|
2
2
|
import type React from "react";
|
|
3
|
-
import { useMemo, useState } from "react";
|
|
3
|
+
import { useMemo, useRef, useState } from "react";
|
|
4
4
|
import { useSafeArea } from "../hooks/useSafeArea";
|
|
5
5
|
import { useAiAssistantI18n } from "../i18n";
|
|
6
6
|
import type { Conversation } from "../types";
|
|
@@ -58,7 +58,7 @@ const StopButton: React.FC<{ onClick: () => void }> = ({ onClick }) => {
|
|
|
58
58
|
|
|
59
59
|
const NewConversationButton: React.FC<{
|
|
60
60
|
disabled: boolean;
|
|
61
|
-
onClick
|
|
61
|
+
onClick?: () => void;
|
|
62
62
|
}> = ({ disabled, onClick }) => {
|
|
63
63
|
const { t } = useAiAssistantI18n();
|
|
64
64
|
return (
|
|
@@ -103,6 +103,7 @@ export const ChatInput: React.FC<ChatInputProps> = ({
|
|
|
103
103
|
}, [lastConv]);
|
|
104
104
|
const disabledNewConversation =
|
|
105
105
|
!hasConversations || lastIsDivider || starting || isLoading;
|
|
106
|
+
const textareaRef = useRef<any>(null);
|
|
106
107
|
|
|
107
108
|
const handleInput = (e: { detail: { value: string } }) => {
|
|
108
109
|
onInputChange(e.detail.value);
|
|
@@ -121,12 +122,31 @@ export const ChatInput: React.FC<ChatInputProps> = ({
|
|
|
121
122
|
} else {
|
|
122
123
|
onInputChange(inputValue + text);
|
|
123
124
|
}
|
|
125
|
+
|
|
126
|
+
// 2. 延迟执行滚动(等渲染完成)
|
|
127
|
+
setTimeout(() => {
|
|
128
|
+
scrollToEndQuickly();
|
|
129
|
+
}, 60); // 50~100ms,根据真机测试微调
|
|
124
130
|
}
|
|
125
131
|
};
|
|
126
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
|
+
|
|
127
147
|
return (
|
|
128
148
|
<View
|
|
129
|
-
className="px-4 py-3
|
|
149
|
+
className="px-4 py-3"
|
|
130
150
|
style={{ paddingBottom: `${8 + safeAreaInsets.bottom}px` }}
|
|
131
151
|
>
|
|
132
152
|
{!isLoading && recommendedQuestions.length > 0 && (
|
|
@@ -147,42 +167,51 @@ export const ChatInput: React.FC<ChatInputProps> = ({
|
|
|
147
167
|
)}
|
|
148
168
|
|
|
149
169
|
<View
|
|
150
|
-
className={`w-full rounded-xl bg-
|
|
170
|
+
className={`w-full rounded-xl bg-background transition-all duration-200 p-3 border ${
|
|
151
171
|
isFocused ? "border-primary shadow-sm" : "border-primary/40"
|
|
152
172
|
}`}
|
|
153
173
|
data-role="chat-input"
|
|
154
174
|
>
|
|
155
175
|
<Textarea
|
|
176
|
+
ref={textareaRef}
|
|
156
177
|
value={inputValue}
|
|
157
178
|
onInput={handleInput}
|
|
158
179
|
onFocus={() => setIsFocused(true)}
|
|
159
180
|
onBlur={() => setIsFocused(false)}
|
|
160
181
|
placeholder={t.inputPlaceholder}
|
|
161
182
|
disabled={isLoading}
|
|
162
|
-
maxlength={
|
|
163
|
-
|
|
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"
|
|
183
|
+
maxlength={10000}
|
|
184
|
+
className="w-full text-base text-foreground placeholder:text-muted-foreground leading-relaxed disabled:opacity-50 resize-none"
|
|
167
185
|
data-role="chat-textarea"
|
|
168
|
-
|
|
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>
|
|
169
201
|
|
|
170
202
|
<View
|
|
171
|
-
className="flex items-center justify-between mt-
|
|
203
|
+
className="flex items-center justify-between mt-1 pt-2"
|
|
172
204
|
data-role="chat-tools"
|
|
173
205
|
>
|
|
174
206
|
<View className="flex items-center gap-2">
|
|
175
207
|
<NewConversationButton
|
|
176
208
|
disabled={disabledNewConversation}
|
|
177
|
-
onClick={onNewConversation
|
|
209
|
+
onClick={onNewConversation}
|
|
178
210
|
/>
|
|
179
211
|
</View>
|
|
180
212
|
|
|
181
213
|
<View className="flex items-center gap-2">
|
|
182
|
-
<VoiceInput
|
|
183
|
-
onResult={handleVoiceResult}
|
|
184
|
-
disabled={starting}
|
|
185
|
-
/>
|
|
214
|
+
<VoiceInput onResult={handleVoiceResult} disabled={starting} />
|
|
186
215
|
{isLoading ? (
|
|
187
216
|
<StopButton onClick={onCancel} />
|
|
188
217
|
) : (
|