@amaster.ai/components-templates 1.8.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 (22) 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 +40 -18
  9. package/components/ai-assistant/template/components/chat-messages.tsx +117 -24
  10. package/components/ai-assistant/template/components/chat-recommends.tsx +79 -15
  11. package/components/ai-assistant/template/components/voice-input.tsx +3 -2
  12. package/components/ai-assistant/template/hooks/useAssistantSize.ts +360 -0
  13. package/components/ai-assistant/template/hooks/useDisplayMode.tsx +52 -5
  14. package/components/ai-assistant/template/hooks/useDraggable.ts +11 -3
  15. package/components/ai-assistant/template/hooks/usePosition.ts +19 -31
  16. package/components/ai-assistant/template/i18n.ts +8 -0
  17. package/components/ai-assistant/template/types.ts +2 -0
  18. package/components/ai-assistant-taro/package.json +16 -8
  19. package/components/ai-assistant-taro/template/components/ChatInput.tsx +5 -12
  20. package/components/ai-assistant-taro/template/components/RecommendedQuestions.tsx +39 -0
  21. package/package.json +1 -1
  22. package/packages/cli/package.json +1 -1
@@ -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.8.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.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",
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",
@@ -4,6 +4,7 @@ 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 {
@@ -151,18 +152,10 @@ export const ChatInput: React.FC<ChatInputProps> = ({
151
152
  >
152
153
  {!isLoading && recommendedQuestions.length > 0 && (
153
154
  <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>
155
+ <RecommendedQuestions
156
+ questions={recommendedQuestions}
157
+ onQuestionClick={onQuestionClick}
158
+ />
166
159
  </View>
167
160
  )}
168
161
 
@@ -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
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amaster.ai/components-templates",
3
- "version": "1.8.0",
3
+ "version": "1.10.0",
4
4
  "description": "Amaster component templates collection",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amaster.ai/cli",
3
- "version": "1.8.0",
3
+ "version": "1.10.0",
4
4
  "description": "Amaster CLI tool for component templates",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",