@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
|
@@ -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}
|
|
@@ -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;
|
|
@@ -134,12 +143,18 @@ const ChatInput: React.FC<ChatInputProps> = ({
|
|
|
134
143
|
textareaRef.current.scrollTop = textareaRef.current.scrollHeight;
|
|
135
144
|
}
|
|
136
145
|
};
|
|
146
|
+
const isHalfScreenMode =
|
|
147
|
+
displayMode === "half-top" || displayMode === "half-bottom";
|
|
148
|
+
const isCompactControls = isMobile || isHalfScreenMode;
|
|
149
|
+
const inputRows = isHalfScreenMode ? 2 : isMobile ? 3 : 4;
|
|
137
150
|
|
|
138
151
|
return (
|
|
139
|
-
<div className="py-3 px-
|
|
152
|
+
<div className={cn("px-4 py-3", isMobile && "px-3 py-2", isHalfScreenMode && "px-3 py-1.5")}>
|
|
140
153
|
<div
|
|
141
154
|
className={cn(
|
|
142
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",
|
|
143
158
|
{
|
|
144
159
|
"border-primary ring-2 ring-primary/20": focus,
|
|
145
160
|
},
|
|
@@ -155,12 +170,16 @@ const ChatInput: React.FC<ChatInputProps> = ({
|
|
|
155
170
|
placeholder={getText().typePlaceholder}
|
|
156
171
|
onFocus={() => setFocus(true)}
|
|
157
172
|
onBlur={() => setFocus(false)}
|
|
158
|
-
className=
|
|
159
|
-
|
|
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}
|
|
160
179
|
disabled={speaking}
|
|
161
180
|
readOnly={speaking}
|
|
162
181
|
/>
|
|
163
|
-
<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")}>
|
|
164
183
|
<div className="flex items-center gap-1">
|
|
165
184
|
{onNew && (
|
|
166
185
|
<NewConvButton
|
|
@@ -168,6 +187,7 @@ const ChatInput: React.FC<ChatInputProps> = ({
|
|
|
168
187
|
disabled={
|
|
169
188
|
!hasConversations || lastIsDivider || starting || isLoading
|
|
170
189
|
}
|
|
190
|
+
compact={isCompactControls}
|
|
171
191
|
/>
|
|
172
192
|
)}
|
|
173
193
|
</div>
|
|
@@ -181,14 +201,16 @@ const ChatInput: React.FC<ChatInputProps> = ({
|
|
|
181
201
|
value={inputValue}
|
|
182
202
|
disabled={isLoading || starting}
|
|
183
203
|
onRunningChange={setSpeaking}
|
|
204
|
+
compact={isCompactControls}
|
|
184
205
|
/>
|
|
185
206
|
{isLoading ? (
|
|
186
|
-
<StopButton onClick={onCancel || (() => {})} />
|
|
207
|
+
<StopButton onClick={onCancel || (() => {})} compact={isCompactControls} />
|
|
187
208
|
) : (
|
|
188
209
|
<SubmitButton
|
|
189
210
|
onClick={() => onSendMessage()}
|
|
190
211
|
disabled={!inputValue.trim() || isLoading || speaking}
|
|
191
212
|
starting={starting}
|
|
213
|
+
compact={isCompactControls}
|
|
192
214
|
/>
|
|
193
215
|
)}
|
|
194
216
|
</div>
|
|
@@ -196,7 +218,7 @@ const ChatInput: React.FC<ChatInputProps> = ({
|
|
|
196
218
|
</div>
|
|
197
219
|
|
|
198
220
|
{(conversations.length > 0 || displayMode !== "inline") && (
|
|
199
|
-
<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">
|
|
200
222
|
{getText().footerAiWarning}
|
|
201
223
|
</p>
|
|
202
224
|
)}
|