@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
@@ -1,24 +1,24 @@
1
1
  {
2
2
  "name": "amaster-react-project",
3
- "version": "1.8.0",
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
- "type-check": "bun scripts/type-check-filter.mjs",
13
- "check:build": "bun scripts/check-build.mjs",
14
- "check:ast": "bun scripts/ast-grep-scan.mjs",
15
- "preview": "vite preview",
16
- "prepare": "bun scripts/prepare.mjs || true"
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.61",
21
- "@amaster.ai/vite-plugins": "1.1.0-beta.61",
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
- "@ast-grep/napi": "^0.40.5",
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 [displayMode, setDisplayMode] = useDisplayMode(isOpen);
19
- const isFullscreen = displayMode === "fullscreen" && isOpen;
20
- const positionHook = usePosition(isOpen, isFullscreen);
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
- style={positionHook.getPositionStyles()}
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 ChatThoughtMessage: React.FC<
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 ChatToolMessage: React.FC<
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 MessageContentRenderer: React.FC<
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
- <ChatThoughtMessage message={message as ThoughtMessage} {...rest} />
167
+ <ChatThoughtContent message={message as ThoughtMessage} {...rest} />
163
168
  );
164
169
  case "tool":
165
- return <ChatToolMessage message={message as ToolMessage} {...rest} />;
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
- <MessageContentRenderer message={message} {...rest} />
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 { Layers2, Maximize, Move, Sidebar } from "lucide-react";
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
- }> = ({ displayMode, onChange }) => {
12
- const modes: {
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
- <HoverCard openDelay={0}>
41
- <HoverCardTrigger asChild>
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
- </HoverCardTrigger>
46
- <HoverCardContent align="end" className="flex flex-col w-fit p-1">
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
- </HoverCardContent>
61
- </HoverCard>
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
- style: React.CSSProperties;
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
- style,
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 !== "floating" ? {} : style}
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
- 'border-b border-border': displayMode === "floating",
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<{ onNew?: () => void; disabled?: boolean }> = ({
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-8 w-8 cursor-pointer"
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
- }> = ({ disabled, starting, onClick }) => {
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 size-8 text-primary-foreground transition-all duration-200 cursor-pointer shadow-sm hover:shadow-2xl",
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<{ onClick: () => void; disabled?: boolean }> = ({
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="size-8 bg-success hover:bg-success/50 text-success-foreground rounded-xl transition-all duration-200 cursor-pointer shadow-sm hover:shadow-2xl"
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-4">
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="w-full text-sm text-foreground placeholder:text-muted-foreground outline-none resize-none leading-5 bg-card"
159
- rows={4}
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-1">
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
  )}