@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.
- 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 +57 -22
- package/components/ai-assistant/template/components/chat-messages.tsx +118 -25
- package/components/ai-assistant/template/components/chat-recommends.tsx +79 -15
- package/components/ai-assistant/template/components/voice-input.tsx +11 -2
- package/components/ai-assistant/template/hooks/useAssistantSize.ts +360 -0
- package/components/ai-assistant/template/hooks/useConversation.ts +0 -23
- 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/ChatAssistantMessage.tsx +24 -2
- package/components/ai-assistant-taro/template/components/ChatInput.tsx +50 -28
- package/components/ai-assistant-taro/template/components/RecommendedQuestions.tsx +39 -0
- 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,24 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "amaster-react-project",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"dev": "vite --force",
|
|
7
7
|
"dev:local": "vite --config vite.config.local.ts",
|
|
8
|
-
"dev:restart": "bun scripts/dev-restart.mjs",
|
|
9
8
|
"build": "vite build",
|
|
10
9
|
"lint": "biome check .",
|
|
11
10
|
"lint:fix": "biome check --write .",
|
|
12
|
-
"
|
|
13
|
-
"check
|
|
14
|
-
"check:
|
|
15
|
-
"
|
|
16
|
-
"
|
|
11
|
+
"pre-commit-check": "amaster-cli check all --stack react-vite",
|
|
12
|
+
"type-check": "amaster-cli check type --stack react-vite",
|
|
13
|
+
"check:build": "amaster-cli check build --stack react-vite",
|
|
14
|
+
"check:ast": "amaster-cli check ast --stack react-vite",
|
|
15
|
+
"check:route": "amaster-cli check route --stack react-vite",
|
|
16
|
+
"preview": "vite preview"
|
|
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.67",
|
|
21
|
+
"@amaster.ai/vite-plugins": "1.1.0-beta.67",
|
|
22
22
|
"@fontsource-variable/inter": "^5.2.8",
|
|
23
23
|
"@fortawesome/fontawesome-free": "^6.1.1",
|
|
24
24
|
"@hookform/resolvers": "^5.2.2",
|
|
@@ -91,9 +91,7 @@
|
|
|
91
91
|
"zod": "^3.24.3"
|
|
92
92
|
},
|
|
93
93
|
"devDependencies": {
|
|
94
|
-
"@
|
|
95
|
-
"@ast-grep/napi-linux-arm64-musl": "^0.40.5",
|
|
96
|
-
"@ast-grep/napi-linux-x64-musl": "^0.40.5",
|
|
94
|
+
"@amaster.ai/cli": "1.1.0-beta.67",
|
|
97
95
|
"@biomejs/biome": "^2.3.4",
|
|
98
96
|
"@types/glob": "^9.0.0",
|
|
99
97
|
"@types/http-proxy": "^1.17.15",
|
|
@@ -9,15 +9,29 @@ import { getText } from "./i18n";
|
|
|
9
9
|
import ChatDisplayModeSwitcher from "./components/chat-display-mode-switcher";
|
|
10
10
|
import { useDisplayMode } from "./hooks/useDisplayMode";
|
|
11
11
|
import InlineAIAssistant from "./inline-ai-assistant";
|
|
12
|
+
import { useIsMobile } from "@/hooks/use-mobile";
|
|
13
|
+
import { useAssistantSize } from "./hooks/useAssistantSize";
|
|
12
14
|
|
|
13
15
|
const AIAssistant: React.FC<{
|
|
14
16
|
greeting?: string;
|
|
15
17
|
recommends?: string[];
|
|
16
18
|
}> = ({ greeting, recommends }) => {
|
|
17
19
|
const [isOpen, setIsOpen] = useState(false);
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
const
|
|
20
|
+
const isMobile = useIsMobile();
|
|
21
|
+
const [displayMode, setDisplayMode] = useDisplayMode(isOpen, isMobile);
|
|
22
|
+
const isLockedLayoutMode =
|
|
23
|
+
displayMode === "fullscreen" ||
|
|
24
|
+
displayMode === "side-left" ||
|
|
25
|
+
displayMode === "side-right" ||
|
|
26
|
+
displayMode === "half-top" ||
|
|
27
|
+
displayMode === "half-bottom";
|
|
28
|
+
const isFullscreen = isLockedLayoutMode && isOpen;
|
|
29
|
+
const sizeHook = useAssistantSize(displayMode);
|
|
30
|
+
const positionHook = usePosition(
|
|
31
|
+
isOpen,
|
|
32
|
+
isFullscreen,
|
|
33
|
+
() => sizeHook.getElementDimensions(isOpen),
|
|
34
|
+
);
|
|
21
35
|
|
|
22
36
|
const draggableHook = useDraggable({
|
|
23
37
|
...positionHook,
|
|
@@ -42,7 +56,30 @@ const AIAssistant: React.FC<{
|
|
|
42
56
|
} else if (displayMode === "side-right") {
|
|
43
57
|
positionHook.setPositionTo("right", "bottom");
|
|
44
58
|
}
|
|
45
|
-
}, [displayMode]);
|
|
59
|
+
}, [displayMode, positionHook.setPositionTo]);
|
|
60
|
+
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
const layoutRoot =
|
|
63
|
+
document.querySelector<HTMLElement>("[data-role='main-layout']") ||
|
|
64
|
+
document.querySelector<HTMLElement>("#root") ||
|
|
65
|
+
document.body;
|
|
66
|
+
|
|
67
|
+
if (!layoutRoot) return;
|
|
68
|
+
|
|
69
|
+
layoutRoot.style.setProperty(
|
|
70
|
+
"--ai-assistant-side-left-width",
|
|
71
|
+
`${sizeHook.sideLeftWidth}px`,
|
|
72
|
+
);
|
|
73
|
+
layoutRoot.style.setProperty(
|
|
74
|
+
"--ai-assistant-side-right-width",
|
|
75
|
+
`${sizeHook.sideRightWidth}px`,
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
return () => {
|
|
79
|
+
layoutRoot.style.removeProperty("--ai-assistant-side-left-width");
|
|
80
|
+
layoutRoot.style.removeProperty("--ai-assistant-side-right-width");
|
|
81
|
+
};
|
|
82
|
+
}, [sizeHook.sideLeftWidth, sizeHook.sideRightWidth]);
|
|
46
83
|
|
|
47
84
|
const handleClose = () => {
|
|
48
85
|
setIsOpen(false);
|
|
@@ -63,11 +100,14 @@ const AIAssistant: React.FC<{
|
|
|
63
100
|
{isOpen && (
|
|
64
101
|
<ChatFloatingCard
|
|
65
102
|
displayMode={displayMode}
|
|
66
|
-
isDragging={isDragging}
|
|
67
|
-
|
|
103
|
+
isDragging={isDragging || sizeHook.isResizing}
|
|
104
|
+
positionStyle={positionHook.getPositionStyles()}
|
|
105
|
+
cardStyle={sizeHook.cardStyle}
|
|
106
|
+
resizeType={sizeHook.resizeType}
|
|
107
|
+
onResizeStart={sizeHook.startResize}
|
|
68
108
|
>
|
|
69
109
|
<ChatHeader
|
|
70
|
-
disabledDrag={displayMode !== "floating"}
|
|
110
|
+
disabledDrag={isMobile || displayMode !== "floating"}
|
|
71
111
|
isDragging={isDragging}
|
|
72
112
|
displayMode={displayMode}
|
|
73
113
|
onMouseDown={handleDragStart}
|
|
@@ -77,6 +117,7 @@ const AIAssistant: React.FC<{
|
|
|
77
117
|
<ChatDisplayModeSwitcher
|
|
78
118
|
displayMode={displayMode}
|
|
79
119
|
onChange={setDisplayMode}
|
|
120
|
+
isMobile={isMobile}
|
|
80
121
|
/>
|
|
81
122
|
</ChatHeader>
|
|
82
123
|
|
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
} from "../types";
|
|
8
8
|
import {
|
|
9
9
|
Check,
|
|
10
|
+
ChevronRight,
|
|
10
11
|
ChevronDown,
|
|
11
12
|
ChevronUp,
|
|
12
13
|
CircleX,
|
|
@@ -26,6 +27,10 @@ interface MessageCommonProps {
|
|
|
26
27
|
isLoading?: boolean;
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
export const isCollapsibleAssistantMessage = (message: MessagesItem) =>
|
|
31
|
+
message.role === "assistant" &&
|
|
32
|
+
(message.kind === "thought" || message.kind === "tool");
|
|
33
|
+
|
|
29
34
|
export const ChatLoading: React.FC<{ className?: string }> = ({
|
|
30
35
|
className,
|
|
31
36
|
}) => {
|
|
@@ -63,7 +68,7 @@ const ChatTextMessage: React.FC<
|
|
|
63
68
|
);
|
|
64
69
|
};
|
|
65
70
|
|
|
66
|
-
const
|
|
71
|
+
export const ChatThoughtContent: React.FC<
|
|
67
72
|
{ message: ThoughtMessage } & MessageCommonProps
|
|
68
73
|
> = ({ message, isNewest, isLoading }) => {
|
|
69
74
|
const [expanded, setExpanded] = useState(false);
|
|
@@ -97,12 +102,12 @@ const ChatThoughtMessage: React.FC<
|
|
|
97
102
|
);
|
|
98
103
|
};
|
|
99
104
|
|
|
100
|
-
const
|
|
105
|
+
export const ChatToolContent: React.FC<
|
|
101
106
|
{ message: ToolMessage } & MessageCommonProps
|
|
102
107
|
> = ({ message, isLoading }) => {
|
|
103
108
|
const status = message.toolStatus || "executing";
|
|
104
109
|
return (
|
|
105
|
-
<div className="leading-relaxed whitespace-pre-wrap break-words flex items-center gap-1 border px-2 p-1 rounded-md bg-muted text-xs max-w-full overflow-hidden">
|
|
110
|
+
<div className="leading-relaxed whitespace-pre-wrap break-words inline-flex items-center gap-1 border px-2 p-1 rounded-md bg-muted text-xs max-w-full overflow-hidden">
|
|
106
111
|
{status === "success" ? (
|
|
107
112
|
<Check className="size-3.5 text-success shrink-0" />
|
|
108
113
|
) : status === "failed" || status === "error" ? (
|
|
@@ -149,7 +154,7 @@ const ChatUIRenderMessage: React.FC<{ message: UIRenderMessage }> = ({
|
|
|
149
154
|
);
|
|
150
155
|
};
|
|
151
156
|
|
|
152
|
-
const
|
|
157
|
+
export const ChatMessageContentRenderer: React.FC<
|
|
153
158
|
{
|
|
154
159
|
message: MessagesItem;
|
|
155
160
|
} & MessageCommonProps
|
|
@@ -159,10 +164,10 @@ const MessageContentRenderer: React.FC<
|
|
|
159
164
|
return <ChatTextMessage message={message as TextMessage} {...rest} />;
|
|
160
165
|
case "thought":
|
|
161
166
|
return (
|
|
162
|
-
<
|
|
167
|
+
<ChatThoughtContent message={message as ThoughtMessage} {...rest} />
|
|
163
168
|
);
|
|
164
169
|
case "tool":
|
|
165
|
-
return <
|
|
170
|
+
return <ChatToolContent message={message as ToolMessage} {...rest} />;
|
|
166
171
|
case "error":
|
|
167
172
|
return <ChatErrorMessage message={message as TextMessage} {...rest} />;
|
|
168
173
|
case "ui-render":
|
|
@@ -198,7 +203,73 @@ const ChatAssistantMessage: React.FC<
|
|
|
198
203
|
<MessageSquare className="h-3.5 w-3.5 text-primary-foreground" strokeWidth={2} />
|
|
199
204
|
)}
|
|
200
205
|
</div>
|
|
201
|
-
<
|
|
206
|
+
<ChatMessageContentRenderer message={message} {...rest} />
|
|
207
|
+
</div>
|
|
208
|
+
);
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
export const ChatAssistantCollapsedGroup: React.FC<
|
|
212
|
+
{
|
|
213
|
+
messages: MessagesItem[];
|
|
214
|
+
showAvatar?: boolean;
|
|
215
|
+
} & MessageCommonProps
|
|
216
|
+
> = ({ messages, showAvatar, isLoading, isNewest }) => {
|
|
217
|
+
const [expanded, setExpanded] = useState(false);
|
|
218
|
+
const thoughtCount = messages.filter((message) => message.kind === "thought").length;
|
|
219
|
+
const toolCount = messages.filter((message) => message.kind === "tool").length;
|
|
220
|
+
const isActive = !!isLoading && !!isNewest;
|
|
221
|
+
|
|
222
|
+
return (
|
|
223
|
+
<div
|
|
224
|
+
className={cn(
|
|
225
|
+
"flex gap-3 animate-in fade-in-0 slide-in-from-bottom-2 duration-300 overflow-hidden w-full chat-assistant-message group",
|
|
226
|
+
)}
|
|
227
|
+
>
|
|
228
|
+
<div
|
|
229
|
+
className={cn(
|
|
230
|
+
"flex-shrink-0 h-7 w-7 rounded-full bg-gradient-to-br from-primary to-primary/40 flex items-center justify-center text-left",
|
|
231
|
+
!showAvatar && "invisible",
|
|
232
|
+
)}
|
|
233
|
+
>
|
|
234
|
+
{showAvatar && (
|
|
235
|
+
<MessageSquare className="h-3.5 w-3.5 text-primary-foreground" strokeWidth={2} />
|
|
236
|
+
)}
|
|
237
|
+
</div>
|
|
238
|
+
<div className="min-w-0 flex-1">
|
|
239
|
+
<div
|
|
240
|
+
className="flex w-full items-center gap-3 px-1 py-2 text-left"
|
|
241
|
+
onClick={() => setExpanded((value) => !value)}
|
|
242
|
+
>
|
|
243
|
+
<div className="flex min-w-0 items-center gap-2">
|
|
244
|
+
{isActive ? (
|
|
245
|
+
<LoaderCircle className="size-4 shrink-0 animate-spin text-primary" />
|
|
246
|
+
) : expanded ? (
|
|
247
|
+
<ChevronDown className="size-4 shrink-0" />
|
|
248
|
+
) : (
|
|
249
|
+
<ChevronRight className="size-4 shrink-0" />
|
|
250
|
+
)}
|
|
251
|
+
</div>
|
|
252
|
+
<span className="shrink-0 text-xs text-muted-foreground">
|
|
253
|
+
{thoughtCount > 0 ? `${thoughtCount} ${getText().thoughtCountUnit}` : ""}
|
|
254
|
+
{thoughtCount > 0 && toolCount > 0 ? " · " : ""}
|
|
255
|
+
{toolCount > 0 ? `${toolCount} ${getText().toolCountUnit}` : ""}
|
|
256
|
+
</span>
|
|
257
|
+
</div>
|
|
258
|
+
|
|
259
|
+
{(expanded || isActive) && (
|
|
260
|
+
<div className="mt-2 flex flex-col gap-2 pl-2">
|
|
261
|
+
{messages.map((message, index) => (
|
|
262
|
+
<div key={message.messageId || `grouped-${index}`} className="min-w-0 text-left">
|
|
263
|
+
<ChatMessageContentRenderer
|
|
264
|
+
message={message}
|
|
265
|
+
isLoading={isLoading}
|
|
266
|
+
isNewest={isNewest && index === messages.length - 1}
|
|
267
|
+
/>
|
|
268
|
+
</div>
|
|
269
|
+
))}
|
|
270
|
+
</div>
|
|
271
|
+
)}
|
|
272
|
+
</div>
|
|
202
273
|
</div>
|
|
203
274
|
);
|
|
204
275
|
};
|
|
@@ -1,15 +1,21 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
Layers2,
|
|
3
|
+
Maximize,
|
|
4
|
+
PanelBottom,
|
|
5
|
+
PanelTop,
|
|
6
|
+
Sidebar,
|
|
7
|
+
} from "lucide-react";
|
|
2
8
|
import { getText } from "../i18n";
|
|
3
|
-
import { HoverCard } from "@radix-ui/react-hover-card";
|
|
4
|
-
import { HoverCardContent, HoverCardTrigger } from "@/components/ui/hover-card";
|
|
5
9
|
import { Button } from "@/components/ui/button";
|
|
10
|
+
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
|
6
11
|
import type { IDisplayMode } from "../types";
|
|
7
12
|
|
|
8
13
|
const ChatDisplayModeSwitcher: React.FC<{
|
|
9
14
|
displayMode: IDisplayMode;
|
|
10
15
|
onChange: (mode: IDisplayMode) => void;
|
|
11
|
-
|
|
12
|
-
|
|
16
|
+
isMobile?: boolean;
|
|
17
|
+
}> = ({ displayMode, onChange, isMobile }) => {
|
|
18
|
+
const desktopModes: {
|
|
13
19
|
mode: IDisplayMode;
|
|
14
20
|
icon: React.ReactNode;
|
|
15
21
|
title: string;
|
|
@@ -35,15 +41,33 @@ const ChatDisplayModeSwitcher: React.FC<{
|
|
|
35
41
|
title: getText().displayMode.fullscreen,
|
|
36
42
|
},
|
|
37
43
|
];
|
|
44
|
+
const mobileModes: typeof desktopModes = [
|
|
45
|
+
{
|
|
46
|
+
mode: "fullscreen",
|
|
47
|
+
icon: <Maximize className="size-4" strokeWidth={1.75} />,
|
|
48
|
+
title: getText().displayMode.fullscreen,
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
mode: "half-top",
|
|
52
|
+
icon: <PanelTop className="size-4" strokeWidth={1.75} />,
|
|
53
|
+
title: getText().displayMode.halfTop,
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
mode: "half-bottom",
|
|
57
|
+
icon: <PanelBottom className="size-4" strokeWidth={1.75} />,
|
|
58
|
+
title: getText().displayMode.halfBottom,
|
|
59
|
+
},
|
|
60
|
+
];
|
|
61
|
+
const modes = isMobile ? mobileModes : desktopModes;
|
|
38
62
|
|
|
39
63
|
return (
|
|
40
|
-
<
|
|
41
|
-
<
|
|
64
|
+
<Popover>
|
|
65
|
+
<PopoverTrigger asChild>
|
|
42
66
|
<Button variant="ghost" size="icon" className="size-8">
|
|
43
67
|
{modes.find((m) => m.mode === displayMode)?.icon}
|
|
44
68
|
</Button>
|
|
45
|
-
</
|
|
46
|
-
<
|
|
69
|
+
</PopoverTrigger>
|
|
70
|
+
<PopoverContent align="end" className="flex w-fit flex-col p-1">
|
|
47
71
|
{modes.map(({ mode, icon, title }) => (
|
|
48
72
|
<Button
|
|
49
73
|
key={mode}
|
|
@@ -57,8 +81,8 @@ const ChatDisplayModeSwitcher: React.FC<{
|
|
|
57
81
|
{title}
|
|
58
82
|
</Button>
|
|
59
83
|
))}
|
|
60
|
-
</
|
|
61
|
-
</
|
|
84
|
+
</PopoverContent>
|
|
85
|
+
</Popover>
|
|
62
86
|
);
|
|
63
87
|
};
|
|
64
88
|
|
|
@@ -19,7 +19,7 @@ const ChatFloatingButton: React.FC<ChatFloatingButtonProps> = ({
|
|
|
19
19
|
}) => {
|
|
20
20
|
return (
|
|
21
21
|
<div
|
|
22
|
-
className={`fixed z-50 flex items-center justify-center ${isDragging ? "cursor-grabbing" : "cursor-grab"}`}
|
|
22
|
+
className={`fixed z-50 flex items-center justify-center touch-none ${isDragging ? "cursor-grabbing" : "cursor-grab"}`}
|
|
23
23
|
style={style}
|
|
24
24
|
>
|
|
25
25
|
<Button
|
|
@@ -35,6 +35,7 @@ const ChatFloatingButton: React.FC<ChatFloatingButtonProps> = ({
|
|
|
35
35
|
border-0
|
|
36
36
|
transition-all duration-300 ease-out select-none
|
|
37
37
|
cursor-pointer
|
|
38
|
+
touch-none
|
|
38
39
|
${
|
|
39
40
|
isDragging
|
|
40
41
|
? "shadow-2xl scale-105"
|
|
@@ -7,14 +7,20 @@ import { cn } from "@/lib/utils";
|
|
|
7
7
|
interface ChatFloatingCardProps {
|
|
8
8
|
displayMode: IDisplayMode;
|
|
9
9
|
isDragging: boolean;
|
|
10
|
-
|
|
10
|
+
positionStyle?: React.CSSProperties;
|
|
11
|
+
cardStyle?: React.CSSProperties;
|
|
12
|
+
resizeType?: "floating-corner" | "side-left" | "side-right" | "half-top" | "half-bottom" | null;
|
|
13
|
+
onResizeStart?: (type: NonNullable<ChatFloatingCardProps["resizeType"]>, event: React.PointerEvent) => void;
|
|
11
14
|
children?: React.ReactNode;
|
|
12
15
|
}
|
|
13
16
|
|
|
14
17
|
const ChatFloatingCard: React.FC<ChatFloatingCardProps> = ({
|
|
15
18
|
displayMode,
|
|
16
19
|
isDragging,
|
|
17
|
-
|
|
20
|
+
positionStyle,
|
|
21
|
+
cardStyle,
|
|
22
|
+
resizeType,
|
|
23
|
+
onResizeStart,
|
|
18
24
|
children,
|
|
19
25
|
}) => {
|
|
20
26
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
@@ -26,6 +32,8 @@ const ChatFloatingCard: React.FC<ChatFloatingCardProps> = ({
|
|
|
26
32
|
`flex flex-col overflow-hidden bg-card border border-border z-50`,
|
|
27
33
|
{
|
|
28
34
|
"fixed inset-0 w-auto h-auto animate-in fade-in-0 zoom-in-[0.98] duration-300 origin-top-left rounded-none": displayMode === "fullscreen",
|
|
35
|
+
"fixed inset-x-0 top-0 h-[50dvh] w-auto rounded-b-2xl rounded-t-none border-x-0 border-t-0": displayMode === "half-top",
|
|
36
|
+
"fixed inset-x-0 bottom-0 h-[50dvh] w-auto rounded-t-2xl rounded-b-none border-x-0 border-b-0": displayMode === "half-bottom",
|
|
29
37
|
"fixed top-0 right-0 w-[420px] h-full rounded-none": displayMode === "side-right",
|
|
30
38
|
"fixed top-0 left-0 w-[420px] h-full rounded-none": displayMode === "side-left",
|
|
31
39
|
"rounded-2xl": displayMode === "floating",
|
|
@@ -33,9 +41,47 @@ const ChatFloatingCard: React.FC<ChatFloatingCardProps> = ({
|
|
|
33
41
|
"shadow-lg": displayMode === "floating" && !isDragging,
|
|
34
42
|
}
|
|
35
43
|
)}
|
|
36
|
-
style={displayMode
|
|
44
|
+
style={displayMode === "floating" ? { ...positionStyle, ...cardStyle } : cardStyle}
|
|
37
45
|
>
|
|
38
46
|
{children}
|
|
47
|
+
{resizeType === "floating-corner" && onResizeStart && (
|
|
48
|
+
<button
|
|
49
|
+
type="button"
|
|
50
|
+
aria-label="Resize chat window"
|
|
51
|
+
className="absolute left-0 top-0 h-6 w-6 cursor-nwse-resize touch-none bg-transparent"
|
|
52
|
+
onPointerDown={(event) => onResizeStart("floating-corner", event)}
|
|
53
|
+
>
|
|
54
|
+
<span className="absolute left-1 top-1 block h-3.5 w-3.5 rounded-sm border-l-2 border-t-2 border-border/70" />
|
|
55
|
+
</button>
|
|
56
|
+
)}
|
|
57
|
+
{resizeType === "side-left" && onResizeStart && (
|
|
58
|
+
<div
|
|
59
|
+
className="absolute right-0 top-0 h-full w-2 cursor-e-resize touch-none"
|
|
60
|
+
onPointerDown={(event) => onResizeStart("side-left", event)}
|
|
61
|
+
/>
|
|
62
|
+
)}
|
|
63
|
+
{resizeType === "side-right" && onResizeStart && (
|
|
64
|
+
<div
|
|
65
|
+
className="absolute left-0 top-0 h-full w-2 cursor-w-resize touch-none"
|
|
66
|
+
onPointerDown={(event) => onResizeStart("side-right", event)}
|
|
67
|
+
/>
|
|
68
|
+
)}
|
|
69
|
+
{resizeType === "half-top" && onResizeStart && (
|
|
70
|
+
<div
|
|
71
|
+
className="absolute bottom-0 left-0 h-3 w-full cursor-ns-resize touch-none"
|
|
72
|
+
onPointerDown={(event) => onResizeStart("half-top", event)}
|
|
73
|
+
>
|
|
74
|
+
<span className="absolute left-1/2 top-1/2 h-1 w-10 -translate-x-1/2 -translate-y-1/2 rounded-full bg-border" />
|
|
75
|
+
</div>
|
|
76
|
+
)}
|
|
77
|
+
{resizeType === "half-bottom" && onResizeStart && (
|
|
78
|
+
<div
|
|
79
|
+
className="absolute top-0 left-0 h-3 w-full cursor-ns-resize touch-none"
|
|
80
|
+
onPointerDown={(event) => onResizeStart("half-bottom", event)}
|
|
81
|
+
>
|
|
82
|
+
<span className="absolute left-1/2 top-1/2 h-1 w-10 -translate-x-1/2 -translate-y-1/2 rounded-full bg-border" />
|
|
83
|
+
</div>
|
|
84
|
+
)}
|
|
39
85
|
</Card>
|
|
40
86
|
);
|
|
41
87
|
};
|
|
@@ -34,7 +34,7 @@ const ChatHeader: React.FC<ChatHeaderProps> = ({
|
|
|
34
34
|
: "cursor-grab"
|
|
35
35
|
: "",
|
|
36
36
|
{
|
|
37
|
-
|
|
37
|
+
"border-b border-border": displayMode !== "inline",
|
|
38
38
|
},
|
|
39
39
|
)}
|
|
40
40
|
onMouseDown={!disabledDrag ? onMouseDown : undefined}
|
|
@@ -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,
|
|
@@ -18,11 +18,13 @@ import {
|
|
|
18
18
|
TooltipTrigger,
|
|
19
19
|
} from "@/components/ui/tooltip";
|
|
20
20
|
import VoiceInputButton from "./voice-input";
|
|
21
|
+
import { useIsMobile } from "@/hooks/use-mobile";
|
|
21
22
|
|
|
22
|
-
const NewConvButton: React.FC<{
|
|
23
|
-
onNew
|
|
24
|
-
disabled
|
|
25
|
-
|
|
23
|
+
const NewConvButton: React.FC<{
|
|
24
|
+
onNew?: () => void;
|
|
25
|
+
disabled?: boolean;
|
|
26
|
+
compact?: boolean;
|
|
27
|
+
}> = ({ onNew, disabled, compact }) => {
|
|
26
28
|
if (!onNew) return null;
|
|
27
29
|
return (
|
|
28
30
|
<TooltipProvider>
|
|
@@ -35,7 +37,7 @@ const NewConvButton: React.FC<{ onNew?: () => void; disabled?: boolean }> = ({
|
|
|
35
37
|
size="icon"
|
|
36
38
|
onClick={onNew}
|
|
37
39
|
disabled={disabled}
|
|
38
|
-
className="h-
|
|
40
|
+
className={cn("cursor-pointer", compact ? "h-7 w-7" : "h-8 w-8")}
|
|
39
41
|
>
|
|
40
42
|
<MessageCirclePlus className="size-5" />
|
|
41
43
|
</Button>
|
|
@@ -49,14 +51,16 @@ const SubmitButton: React.FC<{
|
|
|
49
51
|
disabled: boolean;
|
|
50
52
|
starting?: boolean;
|
|
51
53
|
onClick: () => void;
|
|
52
|
-
|
|
54
|
+
compact?: boolean;
|
|
55
|
+
}> = ({ disabled, starting, onClick, compact }) => {
|
|
53
56
|
return (
|
|
54
57
|
<Button
|
|
55
58
|
onClick={onClick}
|
|
56
59
|
disabled={disabled || starting}
|
|
57
60
|
size="icon"
|
|
58
61
|
className={cn(
|
|
59
|
-
"rounded-xl
|
|
62
|
+
"rounded-xl text-primary-foreground transition-all duration-200 cursor-pointer shadow-sm hover:shadow-2xl",
|
|
63
|
+
compact ? "size-7 rounded-lg" : "size-8",
|
|
60
64
|
"bg-gradient-to-r from-primary to-primary/80",
|
|
61
65
|
"hover:from-primary/90 hover:to-primary/70",
|
|
62
66
|
"disabled:opacity-40 disabled:cursor-not-allowed",
|
|
@@ -72,16 +76,20 @@ const SubmitButton: React.FC<{
|
|
|
72
76
|
);
|
|
73
77
|
};
|
|
74
78
|
|
|
75
|
-
const StopButton: React.FC<{
|
|
76
|
-
onClick
|
|
77
|
-
disabled
|
|
78
|
-
|
|
79
|
+
const StopButton: React.FC<{
|
|
80
|
+
onClick: () => void;
|
|
81
|
+
disabled?: boolean;
|
|
82
|
+
compact?: boolean;
|
|
83
|
+
}> = ({ onClick, disabled, compact }) => {
|
|
79
84
|
return (
|
|
80
85
|
<Button
|
|
81
86
|
onClick={onClick}
|
|
82
87
|
size="icon"
|
|
83
88
|
disabled={disabled}
|
|
84
|
-
className=
|
|
89
|
+
className={cn(
|
|
90
|
+
"bg-success hover:bg-success/50 text-success-foreground transition-all duration-200 cursor-pointer shadow-sm hover:shadow-2xl",
|
|
91
|
+
compact ? "size-7 rounded-lg" : "size-8 rounded-xl",
|
|
92
|
+
)}
|
|
85
93
|
>
|
|
86
94
|
<Square className="h-3 w-3 fill-current" strokeWidth={2} />
|
|
87
95
|
</Button>
|
|
@@ -111,6 +119,7 @@ const ChatInput: React.FC<ChatInputProps> = ({
|
|
|
111
119
|
starting,
|
|
112
120
|
displayMode,
|
|
113
121
|
}) => {
|
|
122
|
+
const isMobile = useIsMobile();
|
|
114
123
|
const hasConversations = conversations.length > 0;
|
|
115
124
|
const lastConv =
|
|
116
125
|
conversations.length > 0 ? conversations[conversations.length - 1] : null;
|
|
@@ -118,6 +127,8 @@ const ChatInput: React.FC<ChatInputProps> = ({
|
|
|
118
127
|
if (!lastConv) return false;
|
|
119
128
|
return lastConv.system?.level === "newConversation";
|
|
120
129
|
}, [lastConv]);
|
|
130
|
+
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
131
|
+
const [speaking, setSpeaking] = useState(false);
|
|
121
132
|
|
|
122
133
|
const [focus, setFocus] = useState(false);
|
|
123
134
|
const handleKeyPress = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
|
@@ -127,29 +138,48 @@ const ChatInput: React.FC<ChatInputProps> = ({
|
|
|
127
138
|
}
|
|
128
139
|
};
|
|
129
140
|
|
|
141
|
+
const textareaScrollToBottom = () => {
|
|
142
|
+
if (textareaRef.current) {
|
|
143
|
+
textareaRef.current.scrollTop = textareaRef.current.scrollHeight;
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
const isHalfScreenMode =
|
|
147
|
+
displayMode === "half-top" || displayMode === "half-bottom";
|
|
148
|
+
const isCompactControls = isMobile || isHalfScreenMode;
|
|
149
|
+
const inputRows = isHalfScreenMode ? 2 : isMobile ? 3 : 4;
|
|
150
|
+
|
|
130
151
|
return (
|
|
131
|
-
<div className="py-3 px-
|
|
152
|
+
<div className={cn("px-4 py-3", isMobile && "px-3 py-2", isHalfScreenMode && "px-3 py-1.5")}>
|
|
132
153
|
<div
|
|
133
154
|
className={cn(
|
|
134
155
|
"w-full rounded-xl bg-card transition-all duration-200 p-3 border border-primary/80",
|
|
156
|
+
isMobile && "p-2.5 rounded-lg",
|
|
157
|
+
isHalfScreenMode && "p-2",
|
|
135
158
|
{
|
|
136
159
|
"border-primary ring-2 ring-primary/20": focus,
|
|
137
160
|
},
|
|
138
161
|
)}
|
|
139
162
|
>
|
|
140
163
|
<textarea
|
|
164
|
+
ref={textareaRef}
|
|
141
165
|
value={inputValue}
|
|
142
166
|
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
|
|
143
167
|
onInputChange(e.target.value)
|
|
144
168
|
}
|
|
145
|
-
onKeyPress={isLoading ? undefined : handleKeyPress}
|
|
169
|
+
onKeyPress={isLoading || speaking ? undefined : handleKeyPress}
|
|
146
170
|
placeholder={getText().typePlaceholder}
|
|
147
171
|
onFocus={() => setFocus(true)}
|
|
148
172
|
onBlur={() => setFocus(false)}
|
|
149
|
-
className=
|
|
150
|
-
|
|
173
|
+
className={cn(
|
|
174
|
+
"w-full text-sm text-foreground placeholder:text-muted-foreground outline-none resize-none leading-5 bg-card",
|
|
175
|
+
isMobile && "text-[13px] leading-5",
|
|
176
|
+
isHalfScreenMode && "text-[13px] leading-4.5",
|
|
177
|
+
)}
|
|
178
|
+
rows={inputRows}
|
|
179
|
+
disabled={speaking}
|
|
180
|
+
readOnly={speaking}
|
|
151
181
|
/>
|
|
152
|
-
<div className="flex items-center justify-between gap-2 mt-
|
|
182
|
+
<div className={cn("mt-1 flex items-center justify-between gap-2", isHalfScreenMode && "mt-0.5")}>
|
|
153
183
|
<div className="flex items-center gap-1">
|
|
154
184
|
{onNew && (
|
|
155
185
|
<NewConvButton
|
|
@@ -157,6 +187,7 @@ const ChatInput: React.FC<ChatInputProps> = ({
|
|
|
157
187
|
disabled={
|
|
158
188
|
!hasConversations || lastIsDivider || starting || isLoading
|
|
159
189
|
}
|
|
190
|
+
compact={isCompactControls}
|
|
160
191
|
/>
|
|
161
192
|
)}
|
|
162
193
|
</div>
|
|
@@ -164,18 +195,22 @@ const ChatInput: React.FC<ChatInputProps> = ({
|
|
|
164
195
|
<VoiceInputButton
|
|
165
196
|
onChange={(text) => {
|
|
166
197
|
onInputChange(text);
|
|
198
|
+
textareaScrollToBottom();
|
|
167
199
|
setFocus(true);
|
|
168
200
|
}}
|
|
169
201
|
value={inputValue}
|
|
170
202
|
disabled={isLoading || starting}
|
|
203
|
+
onRunningChange={setSpeaking}
|
|
204
|
+
compact={isCompactControls}
|
|
171
205
|
/>
|
|
172
|
-
{
|
|
173
|
-
<StopButton onClick={onCancel || (() => {})} />
|
|
206
|
+
{isLoading ? (
|
|
207
|
+
<StopButton onClick={onCancel || (() => {})} compact={isCompactControls} />
|
|
174
208
|
) : (
|
|
175
209
|
<SubmitButton
|
|
176
210
|
onClick={() => onSendMessage()}
|
|
177
|
-
disabled={!inputValue.trim() || isLoading}
|
|
211
|
+
disabled={!inputValue.trim() || isLoading || speaking}
|
|
178
212
|
starting={starting}
|
|
213
|
+
compact={isCompactControls}
|
|
179
214
|
/>
|
|
180
215
|
)}
|
|
181
216
|
</div>
|
|
@@ -183,7 +218,7 @@ const ChatInput: React.FC<ChatInputProps> = ({
|
|
|
183
218
|
</div>
|
|
184
219
|
|
|
185
220
|
{(conversations.length > 0 || displayMode !== "inline") && (
|
|
186
|
-
<p className="text-[10px] text-muted-foreground/50 mt-2 text-center">
|
|
221
|
+
<p className="text-[10px] text-muted-foreground/50 mt-2 text-center leading-4">
|
|
187
222
|
{getText().footerAiWarning}
|
|
188
223
|
</p>
|
|
189
224
|
)}
|