@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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "amaster-react-project",
3
- "version": "1.6.0",
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.59",
21
- "@amaster.ai/vite-plugins": "1.1.0-beta.59",
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
- {!isLoading ? (
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
  )}
@@ -80,7 +80,7 @@ const ChatMessages: React.FC<ChatMessagesProps> = ({
80
80
  }}
81
81
  data-role="chat-messages"
82
82
  >
83
- {isLoadingHistory ? (
83
+ {isLoadingHistory && convLength > 0 ? (
84
84
  <div
85
85
  key="loading-history"
86
86
  className="flex justify-center items-center gap-2 text-center"
@@ -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
- }> = ({ onChange, disabled, value }) => {
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.6.0",
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.57",
43
- "@amaster.ai/client": "1.1.0-beta.57",
44
- "@amaster.ai/taro-echarts-ui": "1.1.0-beta.57",
45
- "@amaster.ai/vite-plugins": "1.1.0-beta.57",
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-lg overflow-hidden text-left">
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-muted text-lg max-w-full overflow-hidden">
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: () => void;
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 border-t border-primary/40 bg-background"
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-primary/10 transition-all duration-200 p-3 border ${
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={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"
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-3 pt-2"
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
  ) : (