@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.
- package/components/ai-assistant/package.json +10 -12
- package/components/ai-assistant/template/ai-assistant.tsx +48 -7
- package/components/ai-assistant/template/components/chat-assistant-message.tsx +78 -7
- package/components/ai-assistant/template/components/chat-display-mode-switcher.tsx +35 -11
- package/components/ai-assistant/template/components/chat-floating-button.tsx +2 -1
- package/components/ai-assistant/template/components/chat-floating-card.tsx +49 -3
- package/components/ai-assistant/template/components/chat-header.tsx +1 -1
- package/components/ai-assistant/template/components/chat-input.tsx +40 -18
- package/components/ai-assistant/template/components/chat-messages.tsx +117 -24
- package/components/ai-assistant/template/components/chat-recommends.tsx +79 -15
- package/components/ai-assistant/template/components/voice-input.tsx +3 -2
- package/components/ai-assistant/template/hooks/useAssistantSize.ts +360 -0
- package/components/ai-assistant/template/hooks/useDisplayMode.tsx +52 -5
- package/components/ai-assistant/template/hooks/useDraggable.ts +11 -3
- package/components/ai-assistant/template/hooks/usePosition.ts +19 -31
- package/components/ai-assistant/template/i18n.ts +8 -0
- package/components/ai-assistant/template/types.ts +2 -0
- package/components/ai-assistant-taro/package.json +16 -8
- package/components/ai-assistant-taro/template/components/ChatInput.tsx +5 -12
- package/components/ai-assistant-taro/template/components/RecommendedQuestions.tsx +39 -0
- package/package.json +1 -1
- 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
|
-
|
|
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
|
-
(
|
|
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
|
|
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:
|
|
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,
|
|
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 = (
|
|
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
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
175
|
-
}
|
|
176
|
-
}, [isOpen, clampToBounds]);
|
|
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",
|
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "taro-project",
|
|
3
|
-
"version": "1.
|
|
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
|
|
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
|
-
"
|
|
28
|
-
"check
|
|
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.
|
|
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.
|
|
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
|
-
<
|
|
155
|
-
{recommendedQuestions
|
|
156
|
-
|
|
157
|
-
|
|
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