@amaster.ai/components-templates 1.6.0 → 1.10.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 (28) hide show
  1. package/components/ai-assistant/package.json +10 -12
  2. package/components/ai-assistant/template/ai-assistant.tsx +48 -7
  3. package/components/ai-assistant/template/components/chat-assistant-message.tsx +78 -7
  4. package/components/ai-assistant/template/components/chat-display-mode-switcher.tsx +35 -11
  5. package/components/ai-assistant/template/components/chat-floating-button.tsx +2 -1
  6. package/components/ai-assistant/template/components/chat-floating-card.tsx +49 -3
  7. package/components/ai-assistant/template/components/chat-header.tsx +1 -1
  8. package/components/ai-assistant/template/components/chat-input.tsx +57 -22
  9. package/components/ai-assistant/template/components/chat-messages.tsx +118 -25
  10. package/components/ai-assistant/template/components/chat-recommends.tsx +79 -15
  11. package/components/ai-assistant/template/components/voice-input.tsx +11 -2
  12. package/components/ai-assistant/template/hooks/useAssistantSize.ts +360 -0
  13. package/components/ai-assistant/template/hooks/useConversation.ts +0 -23
  14. package/components/ai-assistant/template/hooks/useDisplayMode.tsx +52 -5
  15. package/components/ai-assistant/template/hooks/useDraggable.ts +11 -3
  16. package/components/ai-assistant/template/hooks/usePosition.ts +19 -31
  17. package/components/ai-assistant/template/i18n.ts +8 -0
  18. package/components/ai-assistant/template/types.ts +2 -0
  19. package/components/ai-assistant-taro/package.json +16 -8
  20. package/components/ai-assistant-taro/template/components/ChatAssistantMessage.tsx +24 -2
  21. package/components/ai-assistant-taro/template/components/ChatInput.tsx +50 -28
  22. package/components/ai-assistant-taro/template/components/RecommendedQuestions.tsx +39 -0
  23. package/components/ai-assistant-taro/template/components/markdown.tsx +343 -137
  24. package/components/ai-assistant-taro/template/hooks/useConversation.ts +542 -424
  25. package/components/ai-assistant-taro/template/index.tsx +2 -2
  26. package/components/ai-assistant-taro/template/types.ts +16 -0
  27. package/package.json +1 -1
  28. package/packages/cli/package.json +1 -1
@@ -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 }] : [],
@@ -2,13 +2,50 @@ import { useEffect, useState } from "react";
2
2
  import type { IDisplayMode } from "../types";
3
3
 
4
4
  const getStorageKey = () => `${location.hostname}-ai-assistant-display-mode`;
5
+ const MOBILE_DISPLAY_MODES: IDisplayMode[] = [
6
+ "fullscreen",
7
+ "half-top",
8
+ "half-bottom",
9
+ ];
10
+ const DESKTOP_DEFAULT_MODE: IDisplayMode = "floating";
11
+ const MOBILE_DEFAULT_MODE: IDisplayMode = "fullscreen";
5
12
 
6
- export const useDisplayMode = (enabled: boolean) => {
13
+ const normalizeDisplayMode = (
14
+ mode: IDisplayMode | null,
15
+ isMobile: boolean,
16
+ ): IDisplayMode => {
17
+ if (!mode) {
18
+ return isMobile ? MOBILE_DEFAULT_MODE : DESKTOP_DEFAULT_MODE;
19
+ }
20
+
21
+ if (isMobile) {
22
+ return MOBILE_DISPLAY_MODES.includes(mode) ? mode : MOBILE_DEFAULT_MODE;
23
+ }
24
+
25
+ if (mode === "half-top" || mode === "half-bottom") {
26
+ return DESKTOP_DEFAULT_MODE;
27
+ }
28
+
29
+ return mode;
30
+ };
31
+
32
+ export const useDisplayMode = (
33
+ enabled: boolean,
34
+ isMobile: boolean,
35
+ sideWidths?: { left: number; right: number },
36
+ ) => {
7
37
  const STORAGE_KEY = getStorageKey();
8
38
  const [displayMode, setDisplayMode] = useState<IDisplayMode>(
9
- (localStorage.getItem(STORAGE_KEY) as IDisplayMode) || "floating",
39
+ normalizeDisplayMode(
40
+ localStorage.getItem(STORAGE_KEY) as IDisplayMode | null,
41
+ isMobile,
42
+ ),
10
43
  );
11
44
 
45
+ useEffect(() => {
46
+ setDisplayMode((currentMode) => normalizeDisplayMode(currentMode, isMobile));
47
+ }, [isMobile]);
48
+
12
49
  useEffect(() => {
13
50
  localStorage.setItem(STORAGE_KEY, displayMode);
14
51
 
@@ -26,15 +63,25 @@ export const useDisplayMode = (enabled: boolean) => {
26
63
  if (enabled) {
27
64
  if (displayMode === "side-left") {
28
65
  layoutRoot.setAttribute("data-display-mode", "side-left");
66
+ layoutRoot.style.setProperty(
67
+ "--ai-assistant-side-left-width",
68
+ `${sideWidths?.left ?? 420}px`,
69
+ );
29
70
  } else if (displayMode === "side-right") {
30
71
  layoutRoot.setAttribute("data-display-mode", "side-right");
72
+ layoutRoot.style.setProperty(
73
+ "--ai-assistant-side-right-width",
74
+ `${sideWidths?.right ?? 420}px`,
75
+ );
31
76
  }
32
77
  }
33
78
 
34
79
  return () => {
35
80
  removeAttr();
81
+ layoutRoot.style.removeProperty("--ai-assistant-side-left-width");
82
+ layoutRoot.style.removeProperty("--ai-assistant-side-right-width");
36
83
  };
37
- }, [displayMode, enabled]);
84
+ }, [displayMode, enabled, sideWidths?.left, sideWidths?.right]);
38
85
 
39
86
  const removeStyle = () => {
40
87
  const style = document.getElementById("ai-assistant-display-mode-style");
@@ -49,13 +96,13 @@ export const useDisplayMode = (enabled: boolean) => {
49
96
  style.id = "ai-assistant-display-mode-style";
50
97
  style.innerHTML = `
51
98
  [data-display-mode="side-left"] {
52
- padding-left: 420px !important;
99
+ padding-left: var(--ai-assistant-side-left-width, 420px) !important;
53
100
  padding-right: 0 !important;
54
101
  }
55
102
 
56
103
  [data-display-mode="side-right"] {
57
104
  padding-left: 0 !important;
58
- padding-right: 420px !important;
105
+ padding-right: var(--ai-assistant-side-right-width, 420px) !important;
59
106
  }
60
107
  `;
61
108
  document.head.appendChild(style);
@@ -52,6 +52,9 @@ export const useDraggable = ({
52
52
  const touch = e.touches[0];
53
53
  if (!touch) return;
54
54
 
55
+ e.preventDefault();
56
+ e.stopPropagation();
57
+
55
58
  const actualPos = getActualPosition();
56
59
  dragRef.current = {
57
60
  startX: touch.clientX,
@@ -86,15 +89,20 @@ export const useDraggable = ({
86
89
  const handleTouchMove = (e: TouchEvent) => {
87
90
  const touch = e.touches[0];
88
91
  if (touch) {
92
+ e.preventDefault();
89
93
  handleMove(touch.clientX, touch.clientY);
90
94
  }
91
95
  };
92
96
 
93
- const handleEnd = (event) => {
97
+ const handleEnd = (event: MouseEvent | TouchEvent) => {
94
98
  if (dragRef.current) {
95
99
  setPosition((prev) => {
100
+ const fallbackPoint =
101
+ "changedTouches" in event ? event.changedTouches[0] : event;
96
102
  const snapped = snapToEdge(
97
- prev.x === -1 && prev.y === -1 ? { x: event.x, y: event.y } : prev,
103
+ prev.x === -1 && prev.y === -1 && fallbackPoint
104
+ ? { x: fallbackPoint.clientX, y: fallbackPoint.clientY }
105
+ : prev,
98
106
  );
99
107
  savePosition(snapped);
100
108
  return snapped;
@@ -106,7 +114,7 @@ export const useDraggable = ({
106
114
 
107
115
  document.addEventListener("mousemove", handleMouseMove);
108
116
  document.addEventListener("mouseup", handleEnd);
109
- document.addEventListener("touchmove", handleTouchMove, { passive: true });
117
+ document.addEventListener("touchmove", handleTouchMove, { passive: false });
110
118
  document.addEventListener("touchend", handleEnd);
111
119
 
112
120
  return () => {
@@ -1,4 +1,4 @@
1
- import { useCallback, useEffect, useRef, useState } from "react";
1
+ import { useCallback, useEffect, useState } from "react";
2
2
  import type { Position } from "../types";
3
3
 
4
4
  const DEFAULT_POSITION: Position = { x: -1, y: -1 };
@@ -34,23 +34,14 @@ const savePosition = (pos: Position) => {
34
34
  }
35
35
  };
36
36
 
37
- export const usePosition = (isOpen: boolean, isFullscreen: boolean) => {
37
+ export const usePosition = (
38
+ isOpen: boolean,
39
+ isFullscreen: boolean,
40
+ getElementDimensions: () => { width: number; height: number },
41
+ ) => {
38
42
  const [position, setPosition] = useState<Position>(getSavedPosition);
39
- // 使用 ref 来跟踪是否已经初始化,避免重复执行
40
- const initializedRef = useRef(false);
41
43
  const edgeDistance = 16; // 距离边缘的最小距离
42
44
 
43
- const getElementDimensions = useCallback(() => {
44
- const buttonSize = 56;
45
- const dialogWidth = 420;
46
- const dialogHeight =
47
- window.innerHeight < 600 ? window.innerHeight : window.innerHeight - 200;
48
- return {
49
- width: isOpen ? dialogWidth : buttonSize,
50
- height: isOpen ? dialogHeight : buttonSize,
51
- };
52
- }, [isOpen]);
53
-
54
45
  const getActualPosition = useCallback((): Position => {
55
46
  if (position.x < 0 || position.y < 0) {
56
47
  return {
@@ -149,7 +140,8 @@ export const usePosition = (isOpen: boolean, isFullscreen: boolean) => {
149
140
  const handleResize = () => {
150
141
  setPosition((prev) => {
151
142
  if (prev.x < 0 || prev.y < 0) return prev;
152
- return clampToBounds(prev);
143
+ const clamped = clampToBounds(prev);
144
+ return clamped.x === prev.x && clamped.y === prev.y ? prev : clamped;
153
145
  });
154
146
  };
155
147
 
@@ -157,23 +149,19 @@ export const usePosition = (isOpen: boolean, isFullscreen: boolean) => {
157
149
  return () => window.removeEventListener("resize", handleResize);
158
150
  }, [clampToBounds]);
159
151
 
160
- // 修复:只在 isOpen 状态改变时检查边界,避免无限循环
161
152
  useEffect(() => {
162
- if (position.x < 0 || position.y < 0) {
163
- initializedRef.current = true;
164
- return;
165
- }
166
-
167
- // 只在状态变化时执行一次
168
- if (!initializedRef.current) {
169
- const clamped = clampToBounds(position);
170
- if (clamped.x !== position.x || clamped.y !== position.y) {
171
- setPosition(clamped);
153
+ setPosition((prev) => {
154
+ if (prev.x < 0 || prev.y < 0) {
155
+ return prev;
156
+ }
157
+ const clamped = clampToBounds(prev);
158
+ if (clamped.x !== prev.x || clamped.y !== prev.y) {
172
159
  savePosition(clamped);
160
+ return clamped;
173
161
  }
174
- initializedRef.current = true;
175
- }
176
- }, [isOpen, clampToBounds]); // 移除 position 依赖,避免循环
162
+ return prev;
163
+ });
164
+ }, [isOpen, clampToBounds]);
177
165
 
178
166
  const setPositionTo = useCallback(
179
167
  (x: "left" | "right", y: "top" | "bottom") => {
@@ -191,7 +179,7 @@ export const usePosition = (isOpen: boolean, isFullscreen: boolean) => {
191
179
  setPosition(newPos);
192
180
  savePosition(newPos);
193
181
  },
194
- [],
182
+ [getElementDimensions],
195
183
  );
196
184
  return {
197
185
  position,
@@ -15,6 +15,8 @@ const i18nText = {
15
15
  online: "在线",
16
16
  thinking: "思考中",
17
17
  thought: "思考过程",
18
+ thoughtCountUnit: "条思考",
19
+ toolCountUnit: "次工具",
18
20
  newConversation: "开始新对话",
19
21
  fullscreen: "全屏显示",
20
22
  exitFullscreen: "退出全屏",
@@ -30,6 +32,8 @@ const i18nText = {
30
32
  sideLeft: "侧边模式(左)",
31
33
  sideRight: "侧边模式(右)",
32
34
  fullscreen: "全屏模式",
35
+ halfTop: "上半屏",
36
+ halfBottom: "下半屏",
33
37
  },
34
38
  voiceInput: "语音输入",
35
39
  stopVoiceInput: "停止录音",
@@ -72,6 +76,8 @@ const i18nText = {
72
76
  online: "Online",
73
77
  thinking: "Thinking",
74
78
  thought: "Thought process",
79
+ thoughtCountUnit: "thoughts",
80
+ toolCountUnit: "tools",
75
81
  newConversation: "Start a new chat",
76
82
  fullscreen: "Fullscreen",
77
83
  exitFullscreen: "Exit fullscreen",
@@ -88,6 +94,8 @@ const i18nText = {
88
94
  sideLeft: "Side mode (left)",
89
95
  sideRight: "Side mode (right)",
90
96
  fullscreen: "Fullscreen mode",
97
+ halfTop: "Top half",
98
+ halfBottom: "Bottom half",
91
99
  },
92
100
  voiceInput: "Voice input",
93
101
  stopVoiceInput: "Stop recording",
@@ -75,6 +75,8 @@ export type IDisplayMode =
75
75
  | "floating"
76
76
  | "side-left"
77
77
  | "side-right"
78
+ | "half-top"
79
+ | "half-bottom"
78
80
  | "inline";
79
81
 
80
82
  export interface InlineAIAssistantProps {
@@ -1,16 +1,22 @@
1
1
  {
2
2
  "name": "taro-project",
3
- "version": "1.6.0",
3
+ "version": "1.10.0",
4
4
  "description": "开箱即用的基于Taro + React + Zustand + TailwindCSS + TypeScript的模板",
5
5
  "author": "amaster.ai",
6
6
  "license": "MIT",
7
+ "packageManager": "bun@1.2.21",
7
8
  "templateInfo": {
8
9
  "name": "default",
9
10
  "typescript": true,
10
11
  "css": "sass"
11
12
  },
12
13
  "scripts": {
13
- "dev": "bun run build:h5 -- --watch",
14
+ "dev": "bun run preview:h5",
15
+ "preview:h5": "taro build --type h5 --watch",
16
+ "dev:h5": "bun run preview:h5",
17
+ "dev:weapp": "taro build --type weapp --watch",
18
+ "build": "bun run build:weapp",
19
+ "build:mini": "bun run build:weapp",
14
20
  "build:weapp": "taro build --type weapp",
15
21
  "build:swan": "taro build --type swan",
16
22
  "build:alipay": "taro build --type alipay",
@@ -24,8 +30,9 @@
24
30
  "lint:check": "biome check .",
25
31
  "lint:fix": "biome check --write --unsafe .",
26
32
  "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",
33
+ "pre-commit-check": "amaster-cli check all --stack taro",
34
+ "type-check": "amaster-cli check type --stack taro",
35
+ "check:build": "amaster-cli check build --stack taro",
29
36
  "precommit": "bun run lint:check && bun run type-check",
30
37
  "prepare": "husky || true",
31
38
  "postinstall": "weapp-tw patch || true",
@@ -39,10 +46,10 @@
39
46
  ],
40
47
  "dependencies": {
41
48
  "@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",
49
+ "@amaster.ai/bpm-ui": "1.1.0-beta.67",
50
+ "@amaster.ai/client": "1.1.0-beta.67",
51
+ "@amaster.ai/taro-echarts-ui": "1.1.0-beta.67",
52
+ "@amaster.ai/vite-plugins": "1.1.0-beta.67",
46
53
  "@babel/runtime": "^7.28.3",
47
54
  "@tarojs/components": "4.1.5",
48
55
  "@tarojs/helper": "4.1.5",
@@ -69,6 +76,7 @@
69
76
  "@egoist/tailwindcss-icons": "^1.9.0",
70
77
  "@iconify-json/lucide": "^1.2.64",
71
78
  "@iconify-json/mdi": "^1.2.3",
79
+ "@amaster.ai/cli": "1.1.0-beta.67",
72
80
  "@tarojs/cli": "4.1.5",
73
81
  "@tarojs/taro-loader": "4.1.5",
74
82
  "@tarojs/vite-runner": "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,9 +1,10 @@
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";
7
+ import { RecommendedQuestions } from "./RecommendedQuestions";
7
8
  import { VoiceInput } from "./VoiceInput";
8
9
 
9
10
  interface ChatInputProps {
@@ -58,7 +59,7 @@ const StopButton: React.FC<{ onClick: () => void }> = ({ onClick }) => {
58
59
 
59
60
  const NewConversationButton: React.FC<{
60
61
  disabled: boolean;
61
- onClick: () => void;
62
+ onClick?: () => void;
62
63
  }> = ({ disabled, onClick }) => {
63
64
  const { t } = useAiAssistantI18n();
64
65
  return (
@@ -103,6 +104,7 @@ export const ChatInput: React.FC<ChatInputProps> = ({
103
104
  }, [lastConv]);
104
105
  const disabledNewConversation =
105
106
  !hasConversations || lastIsDivider || starting || isLoading;
107
+ const textareaRef = useRef<any>(null);
106
108
 
107
109
  const handleInput = (e: { detail: { value: string } }) => {
108
110
  onInputChange(e.detail.value);
@@ -121,68 +123,88 @@ export const ChatInput: React.FC<ChatInputProps> = ({
121
123
  } else {
122
124
  onInputChange(inputValue + text);
123
125
  }
126
+
127
+ // 2. 延迟执行滚动(等渲染完成)
128
+ setTimeout(() => {
129
+ scrollToEndQuickly();
130
+ }, 60); // 50~100ms,根据真机测试微调
124
131
  }
125
132
  };
126
133
 
134
+ const scrollToEndQuickly = () => {
135
+ const ta = textareaRef.current;
136
+ if (!ta) return;
137
+
138
+ const len = inputValue.length;
139
+
140
+ ta.focus?.();
141
+ ta.setSelectionRange?.(len, len);
142
+
143
+ setTimeout(() => {
144
+ ta.blur?.();
145
+ }, 30);
146
+ };
147
+
127
148
  return (
128
149
  <View
129
- className="px-4 py-3 border-t border-primary/40 bg-background"
150
+ className="px-4 py-3"
130
151
  style={{ paddingBottom: `${8 + safeAreaInsets.bottom}px` }}
131
152
  >
132
153
  {!isLoading && recommendedQuestions.length > 0 && (
133
154
  <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>
155
+ <RecommendedQuestions
156
+ questions={recommendedQuestions}
157
+ onQuestionClick={onQuestionClick}
158
+ />
146
159
  </View>
147
160
  )}
148
161
 
149
162
  <View
150
- className={`w-full rounded-xl bg-primary/10 transition-all duration-200 p-3 border ${
163
+ className={`w-full rounded-xl bg-background transition-all duration-200 p-3 border ${
151
164
  isFocused ? "border-primary shadow-sm" : "border-primary/40"
152
165
  }`}
153
166
  data-role="chat-input"
154
167
  >
155
168
  <Textarea
169
+ ref={textareaRef}
156
170
  value={inputValue}
157
171
  onInput={handleInput}
158
172
  onFocus={() => setIsFocused(true)}
159
173
  onBlur={() => setIsFocused(false)}
160
174
  placeholder={t.inputPlaceholder}
161
175
  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"
176
+ maxlength={10000}
177
+ className="w-full text-base text-foreground placeholder:text-muted-foreground leading-relaxed disabled:opacity-50 resize-none"
167
178
  data-role="chat-textarea"
168
- />
179
+ confirm-type="send"
180
+ style={{
181
+ height: "60px",
182
+ minHeight: "60px",
183
+ maxHeight: "60px",
184
+ resize: "none",
185
+ background: "none",
186
+ fontSize: "14px",
187
+ }}
188
+ >
189
+ {/* 不要删掉,这是为了解决 h5 textarea 样式问题 */}
190
+ <style>
191
+ {`.taro-textarea {background: none;resize: none;}`}
192
+ </style>
193
+ </Textarea>
169
194
 
170
195
  <View
171
- className="flex items-center justify-between mt-3 pt-2"
196
+ className="flex items-center justify-between mt-1 pt-2"
172
197
  data-role="chat-tools"
173
198
  >
174
199
  <View className="flex items-center gap-2">
175
200
  <NewConversationButton
176
201
  disabled={disabledNewConversation}
177
- onClick={onNewConversation!}
202
+ onClick={onNewConversation}
178
203
  />
179
204
  </View>
180
205
 
181
206
  <View className="flex items-center gap-2">
182
- <VoiceInput
183
- onResult={handleVoiceResult}
184
- disabled={starting}
185
- />
207
+ <VoiceInput onResult={handleVoiceResult} disabled={starting} />
186
208
  {isLoading ? (
187
209
  <StopButton onClick={onCancel} />
188
210
  ) : (
@@ -0,0 +1,39 @@
1
+ import { ScrollView, Text, View } from "@tarojs/components";
2
+ import type React from "react";
3
+
4
+ interface RecommendedQuestionsProps {
5
+ questions: string[];
6
+ onQuestionClick?: (question: string) => void;
7
+ }
8
+
9
+ export const RecommendedQuestions: React.FC<RecommendedQuestionsProps> = ({
10
+ questions,
11
+ onQuestionClick,
12
+ }) => {
13
+ if (questions.length === 0) {
14
+ return null;
15
+ }
16
+
17
+ return (
18
+ <ScrollView
19
+ scrollX
20
+ enhanced
21
+ showScrollbar={false}
22
+ enableFlex
23
+ className="w-full"
24
+ >
25
+ <View className="flex w-max gap-2 pr-4">
26
+ {questions.map((question) => (
27
+ <View
28
+ key={question}
29
+ onClick={() => onQuestionClick?.(question)}
30
+ className="shrink-0 text-xs px-3 py-1.5 bg-primary/20 text-primary rounded-full border border-primary/40 active:bg-primary/30"
31
+ hoverClass="bg-primary/40"
32
+ >
33
+ <Text className="whitespace-nowrap">{question}</Text>
34
+ </View>
35
+ ))}
36
+ </View>
37
+ </ScrollView>
38
+ );
39
+ };