Unverified 提交 065bab97 authored 作者: Mohamed Aziz Mejri's avatar Mohamed Aziz Mejri 提交者: GitHub

Improving chat input aesthetics (#2561)

<!-- devin-review-badge-begin --> --- <a href="https://app.devin.ai/review/dyad-sh/dyad/pull/2561" target="_blank"> <picture> <source media="(prefers-color-scheme: dark)" srcset="https://static.devin.ai/assets/gh-open-in-devin-review-dark.svg?v=1"> <img src="https://static.devin.ai/assets/gh-open-in-devin-review-light.svg?v=1" alt="Open with Devin"> </picture> </a> <!-- devin-review-badge-end --> <!-- This is an auto-generated description by cubic. --> --- ## Summary by cubic Refreshes the chat input UI for a cleaner look and consistent controls. Adds per‑mode icons and clearer visual states across inputs, pickers, and actions. - **UI Improvements** - Unified control sizing (h-7, text-xs, rounded-lg); vertical align center; input row aligns to end; removed spacer divs. - Borderless triggers for Mode/Model/Tools/Pro with subtle hover; smaller icons; muted “Model:”; mode chips show icons for all modes; Ask/Plan get color accents. - Input boxes rounded-2xl with focus-within ring; home input gets hover border; context banner rounded-t-2xl. - Send/Cancel use text-color hovers with clearer disabled; Cancel turns destructive on hover; Auxiliary Actions is circular with gentle hover scale. - Increased input padding and 15px text with repositioned placeholder; trimmed outer and MessagesList padding for tighter alignment; home send icon switched to SendHorizontal. <sup>Written for commit 301875efc229e998e9ba52b2eeeac057bd017b16. Summary will update on new commits.</sup> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: 's avatarWill Chen <willchen90@gmail.com>
上级 fd27826e
......@@ -82,6 +82,7 @@ The stashed changes will be automatically merged back after the rebase completes
- If both sides of a conflict have valid imports/hooks, keep both and remove any duplicate constant redefinitions
- When rebasing documentation/table conflicts (e.g., workflow README tables), prefer keeping **both** additions from HEAD and upstream - merge new rows/content from both branches rather than choosing one side
- **Complementary additions**: When both sides added new sections at the end of a file (e.g., both added different documentation tips), keep both sections rather than choosing one — they're not truly conflicting, just different additions
- **React component wrapper conflicts**: When rebasing UI changes that conflict on wrapper div classes (e.g., `flex items-start space-x-2` vs `flex items-end gap-1`), keep the newer styling from the incoming commit but preserve any functional components (like dialogs or modals) that exist in HEAD but not in the incoming change
## Rebasing with uncommitted changes
......
......@@ -23,7 +23,7 @@ export function ChatInputControls({
(settings?.selectedChatMode === "build" && enabledMcpServersCount > 0);
return (
<div className="flex">
<div className="flex items-center">
<ChatModeSelector />
{showMcpToolsPicker && (
<>
......@@ -33,15 +33,8 @@ export function ChatInputControls({
)}
<div className="w-1.5"></div>
<ModelPicker />
<div className="w-1.5"></div>
<ProModeSelector />
<div className="w-1"></div>
{showContextFilesPicker && (
<>
<ContextFilesPicker />
<div className="w-0.5"></div>
</>
)}
{showContextFilesPicker && <ContextFilesPicker />}
</div>
);
}
......@@ -22,6 +22,7 @@ import { toast } from "sonner";
import { LocalAgentNewChatToast } from "./LocalAgentNewChatToast";
import { useAtomValue } from "jotai";
import { chatMessagesByIdAtom } from "@/atoms/chatAtoms";
import { Hammer, Bot, MessageCircle, Lightbulb } from "lucide-react";
function NewBadge() {
return (
......@@ -95,6 +96,22 @@ export function ChatModeSelector() {
return "Build";
}
};
const getModeIcon = (mode: ChatMode) => {
switch (mode) {
case "build":
case "agent":
return <Hammer size={14} />;
case "ask":
return <MessageCircle size={14} />;
case "local-agent":
return <Bot size={14} />;
case "plan":
return <Lightbulb size={14} />;
default:
return <Hammer size={14} />;
}
};
const isMac = detectIsMac();
return (
......@@ -109,18 +126,25 @@ export function ChatModeSelector() {
<MiniSelectTrigger
data-testid="chat-mode-selector"
className={cn(
"h-6 w-fit px-1.5 py-0 text-xs-sm font-medium shadow-none gap-0.5",
selectedMode === "build" ||
selectedMode === "local-agent" ||
selectedMode === "plan"
? "bg-background hover:bg-muted/50 focus:bg-muted/50"
: "bg-primary/10 hover:bg-primary/20 focus:bg-primary/20 text-primary border-primary/20 dark:bg-primary/20 dark:hover:bg-primary/30 dark:focus:bg-primary/30",
"cursor-pointer w-fit px-2 py-0 text-xs font-medium border-none shadow-none gap-1 rounded-lg transition-colors",
selectedMode === "build" || selectedMode === "local-agent"
? "text-foreground/80 hover:text-foreground hover:bg-muted/60"
: selectedMode === "ask"
? "bg-purple-500/10 text-purple-600 hover:bg-purple-500/15 dark:bg-purple-500/15 dark:text-purple-400 dark:hover:bg-purple-500/20"
: selectedMode === "plan"
? "bg-blue-500/10 text-blue-600 hover:bg-blue-500/15 dark:bg-blue-500/15 dark:text-blue-400 dark:hover:bg-blue-500/20"
: "text-foreground/80 hover:text-foreground hover:bg-muted/60",
)}
size="sm"
/>
}
>
<SelectValue>{getModeDisplayName(selectedMode)}</SelectValue>
<SelectValue>
<span className="flex items-center gap-1.5">
{getModeIcon(selectedMode)}
{getModeDisplayName(selectedMode)}
</span>
</SelectValue>
</TooltipTrigger>
<TooltipContent>
{`Open mode menu (${isMac ? "\u2318 + ." : "Ctrl + ."} to toggle)`}
......@@ -131,10 +155,11 @@ export function ChatModeSelector() {
<SelectItem value="local-agent">
<div className="flex flex-col items-start">
<div className="flex items-center gap-1.5">
<Bot size={14} className="text-muted-foreground" />
<span className="font-medium">Agent v2</span>
<NewBadge />
</div>
<span className="text-xs text-muted-foreground">
<span className="text-xs text-muted-foreground ml-[22px]">
Better at bigger tasks and debugging
</span>
</div>
......@@ -143,10 +168,11 @@ export function ChatModeSelector() {
<SelectItem value="plan">
<div className="flex flex-col items-start">
<div className="flex items-center gap-1.5">
<Lightbulb size={14} className="text-blue-500" />
<span className="font-medium">Plan</span>
<NewBadge />
</div>
<span className="text-xs text-muted-foreground">
<span className="text-xs text-muted-foreground ml-[22px]">
Design before you build
</span>
</div>
......@@ -155,13 +181,14 @@ export function ChatModeSelector() {
<SelectItem value="local-agent" disabled={isQuotaExceeded}>
<div className="flex flex-col items-start">
<div className="flex items-center gap-1.5">
<Bot size={14} className="text-muted-foreground" />
<span className="font-medium">Basic Agent</span>
<span className="text-xs text-muted-foreground">
({isQuotaExceeded ? "0" : messagesRemaining}/5 remaining for
today)
</span>
</div>
<span className="text-xs text-muted-foreground">
<span className="text-xs text-muted-foreground ml-[22px]">
{isQuotaExceeded
? "Daily limit reached"
: "Try our AI agent for free"}
......@@ -171,16 +198,22 @@ export function ChatModeSelector() {
)}
<SelectItem value="build">
<div className="flex flex-col items-start">
<div className="flex items-center gap-1.5">
<Hammer size={14} className="text-muted-foreground" />
<span className="font-medium">Build</span>
<span className="text-xs text-muted-foreground">
</div>
<span className="text-xs text-muted-foreground ml-[22px]">
Generate and edit code
</span>
</div>
</SelectItem>
<SelectItem value="ask">
<div className="flex flex-col items-start">
<div className="flex items-center gap-1.5">
<MessageCircle size={14} className="text-purple-500" />
<span className="font-medium">Ask</span>
<span className="text-xs text-muted-foreground">
</div>
<span className="text-xs text-muted-foreground ml-[22px]">
Ask questions about the app
</span>
</div>
......
......@@ -25,11 +25,11 @@ export function McpToolsPicker() {
return (
<Popover open={isOpen} onOpenChange={setIsOpen}>
<PopoverTrigger
className="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground h-8 px-2"
className="inline-flex items-center justify-center whitespace-nowrap rounded-lg text-xs font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 border-none bg-transparent shadow-none text-muted-foreground hover:text-foreground hover:bg-muted/60 h-7 px-1.5 cursor-pointer"
data-testid="mcp-tools-button"
title="Tools"
>
<Wrench className="size-4" />
<Wrench className="size-3.5" />
</PopoverTrigger>
<PopoverContent
className="w-120 max-h-[80vh] overflow-y-auto"
......
......@@ -166,14 +166,16 @@ export function ModelPicker() {
return (
<DropdownMenu open={open} onOpenChange={setOpen}>
<DropdownMenuTrigger
className="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground h-8 max-w-[130px] px-1.5 text-xs-sm gap-2"
className="inline-flex items-center justify-center whitespace-nowrap rounded-lg text-xs font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 border-none bg-transparent shadow-none text-foreground/80 hover:text-foreground hover:bg-muted/60 h-7 max-w-[130px] px-2 gap-1.5 cursor-pointer"
data-testid="model-picker"
title={modelDisplayName}
>
<span className="truncate">
{modelDisplayName === "Auto" && (
<>
<span className="text-xs text-muted-foreground">Model:</span>{" "}
<span className="text-xs text-muted-foreground/70">
Model:
</span>{" "}
</>
)}
{modelDisplayName}
......
......@@ -71,11 +71,11 @@ export function ProModeSelector() {
<Tooltip>
<TooltipTrigger
render={
<PopoverTrigger className="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 border border-primary/50 bg-background shadow-sm hover:bg-primary/10 h-8 px-1.5 gap-1.5 shadow-primary/10 hover:shadow-md hover:shadow-primary/15" />
<PopoverTrigger className="inline-flex items-center justify-center whitespace-nowrap rounded-lg text-xs font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 border-none bg-transparent shadow-none text-primary/95 hover:text-primary hover:bg-primary/10 h-7 px-2 gap-1 cursor-pointer" />
}
>
<Sparkles className="h-4 w-4 text-primary" />
<span className="text-primary font-medium text-xs-sm">Pro</span>
<Sparkles className="h-3.5 w-3.5" />
<span className="font-medium">Pro</span>
</TooltipTrigger>
<TooltipContent>Configure Dyad Pro settings</TooltipContent>
</Tooltip>
......
......@@ -132,7 +132,7 @@ export function AuxiliaryActionsMenu({
<>
<DropdownMenu open={isOpen} onOpenChange={setIsOpen}>
<DropdownMenuTrigger
className="inline-flex items-center justify-center whitespace-nowrap rounded-xl text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 hover:bg-muted bg-primary/10 text-primary cursor-pointer h-8 px-2"
className="inline-flex items-center justify-center whitespace-nowrap rounded-full text-sm font-medium transition-all duration-200 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 hover:bg-primary/20 hover:scale-105 bg-primary/10 text-primary cursor-pointer h-8 w-8 mb-1"
data-testid="auxiliary-actions-menu"
>
<Plus
......
......@@ -92,6 +92,7 @@ import { useCountTokens } from "@/hooks/useCountTokens";
import { useChats } from "@/hooks/useChats";
import { useRouter } from "@tanstack/react-router";
import { showError as showErrorToast } from "@/lib/toast";
import { cn } from "@/lib/utils";
const showTokenBarAtom = atom(false);
......@@ -424,7 +425,7 @@ export function ChatInput({ chatId }: { chatId?: number }) {
{t("errorLoadingProposal", { message: proposalError.message })}
</div>
)}
<div className="p-4" data-testid="chat-input-container">
<div className="p-2 pt-0" data-testid="chat-input-container">
{/* Show context limit banner above chat input for visibility */}
{showBanner && tokenCountResult && (
<ContextLimitBanner
......@@ -433,9 +434,12 @@ export function ChatInput({ chatId }: { chatId?: number }) {
/>
)}
<div
className={`relative flex flex-col border border-border rounded-lg bg-(--background-lighter) shadow-sm ${
isDraggingOver ? "ring-2 ring-blue-500 border-blue-500" : ""
} ${showBanner ? "rounded-t-none border-t-0" : ""}`}
className={cn(
"relative flex flex-col border border-border rounded-2xl bg-(--background-lighter) transition-colors duration-200",
"focus-within:border-primary/30 focus-within:ring-1 focus-within:ring-primary/20",
isDraggingOver && "ring-2 ring-blue-500 border-blue-500",
showBanner && "rounded-t-none border-t-0",
)}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
......@@ -566,7 +570,7 @@ export function ChatInput({ chatId }: { chatId?: number }) {
onCancel={cancelPendingFiles}
/>
<div className="flex items-start space-x-2 ">
<div className="flex items-end gap-1">
<LexicalChatInput
value={inputValue}
onChange={setInputValue}
......@@ -585,7 +589,7 @@ export function ChatInput({ chatId }: { chatId?: number }) {
<button
onClick={handleCancel}
aria-label={t("cancelGeneration")}
className="px-2 py-2 mt-1 mr-1 hover:bg-(--background-darkest) text-(--sidebar-accent-fg) rounded-lg"
className="px-2 py-2 mb-0.5 mr-1 text-muted-foreground hover:text-destructive rounded-lg transition-colors duration-150 cursor-pointer"
/>
}
>
......@@ -604,7 +608,7 @@ export function ChatInput({ chatId }: { chatId?: number }) {
disableSendButton
}
aria-label={t("sendMessage")}
className="px-2 py-2 mt-1 mr-1 hover:bg-(--background-darkest) text-(--sidebar-accent-fg) rounded-lg disabled:opacity-50"
className="px-2 py-2 mb-0.5 mr-1 text-muted-foreground hover:text-primary rounded-lg transition-colors duration-150 disabled:opacity-30 disabled:hover:text-muted-foreground cursor-pointer disabled:cursor-default"
/>
}
>
......@@ -614,7 +618,7 @@ export function ChatInput({ chatId }: { chatId?: number }) {
</Tooltip>
)}
</div>
<div className="pl-2 pr-1 flex items-center justify-between pb-2">
<div className="px-2 flex items-center justify-between pb-0.5 pt-0.5">
<div className="flex items-center">
<ChatInputControls showContextFilesPicker={false} />
</div>
......
......@@ -50,7 +50,7 @@ export function ContextLimitBanner({
return (
<div
className="mx-auto max-w-3xl px-3 py-1.5 rounded-t-md border-t border-l border-r border-amber-500/30 bg-amber-500/10 flex items-center justify-between gap-3 text-xs text-amber-600 dark:text-amber-500"
className="mx-auto max-w-3xl px-3 py-1.5 rounded-t-2xl border-t border-l border-r border-amber-500/30 bg-amber-500/10 flex items-center justify-between gap-3 text-xs text-amber-600 dark:text-amber-500"
data-testid="context-limit-banner"
>
<span className="flex items-center gap-1.5">
......
import { SendIcon, StopCircleIcon } from "lucide-react";
import { SendHorizontalIcon, StopCircleIcon } from "lucide-react";
import {
Tooltip,
TooltipTrigger,
......@@ -20,6 +20,7 @@ import { LexicalChatInput } from "./LexicalChatInput";
import { useChatModeToggle } from "@/hooks/useChatModeToggle";
import { useTypingPlaceholder } from "@/hooks/useTypingPlaceholder";
import { AuxiliaryActionsMenu } from "./AuxiliaryActionsMenu";
import { cn } from "@/lib/utils";
export function HomeChatInput({
onSubmit,
......@@ -85,9 +86,12 @@ export function HomeChatInput({
<>
<div className="p-4" data-testid="home-chat-input-container">
<div
className={`relative flex flex-col space-y-2 border border-border rounded-lg bg-(--background-lighter) shadow-sm ${
isDraggingOver ? "ring-2 ring-blue-500 border-blue-500" : ""
}`}
className={cn(
"relative flex flex-col border border-border rounded-2xl bg-(--background-lighter) transition-colors duration-200",
"hover:border-primary/30",
"focus-within:border-primary/30 focus-within:ring-1 focus-within:ring-primary/20",
isDraggingOver && "ring-2 ring-blue-500 border-blue-500",
)}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
......@@ -108,7 +112,7 @@ export function HomeChatInput({
onCancel={cancelPendingFiles}
/>
<div className="flex items-start space-x-2 ">
<div className="flex items-end gap-1">
<LexicalChatInput
value={inputValue}
onChange={setInputValue}
......@@ -127,7 +131,7 @@ export function HomeChatInput({
render={
<button
aria-label="Cancel generation (unavailable here)"
className="px-2 py-2 mt-1 mr-1 text-(--sidebar-accent-fg) rounded-lg opacity-50 cursor-not-allowed"
className="px-2 py-2 mb-0.5 mr-1 text-muted-foreground rounded-lg opacity-50 cursor-not-allowed transition-colors duration-150"
/>
}
>
......@@ -145,17 +149,17 @@ export function HomeChatInput({
onClick={handleCustomSubmit}
disabled={!inputValue.trim() && attachments.length === 0}
aria-label="Send message"
className="px-2 py-2 mt-1 mr-1 hover:bg-(--background-darkest) text-(--sidebar-accent-fg) rounded-lg disabled:opacity-50"
className="px-2 py-2 mb-0.5 mr-1 text-muted-foreground hover:text-primary rounded-lg transition-colors duration-150 disabled:opacity-30 disabled:hover:text-muted-foreground cursor-pointer disabled:cursor-default"
/>
}
>
<SendIcon size={20} />
<SendHorizontalIcon size={20} />
</TooltipTrigger>
<TooltipContent>Send message</TooltipContent>
</Tooltip>
)}
</div>
<div className="pl-2 pr-1 flex items-center justify-between pb-2">
<div className="px-2 flex items-center justify-between pb-0.5 pt-0.5">
<div className="flex items-center">
<ChatInputControls showContextFilesPicker={false} />
</div>
......
......@@ -450,10 +450,10 @@ export function LexicalChatInput({
<PlainTextPlugin
contentEditable={
<ContentEditable
className="flex-1 p-2 focus:outline-none overflow-y-auto min-h-[40px] max-h-[200px] resize-none"
className="flex-1 px-3 pt-3 pb-2 focus:outline-none overflow-y-auto min-h-[44px] max-h-[200px] resize-none text-[15px]"
aria-placeholder={placeholder}
placeholder={
<div className="absolute top-2 left-2 text-muted-foreground pointer-events-none select-none">
<div className="absolute top-3 left-3 text-muted-foreground pointer-events-none select-none text-[15px]">
{placeholder}
</div>
}
......
......@@ -347,7 +347,7 @@ export const MessagesList = forwardRef<HTMLDivElement, MessagesListProps>(
if (setupBanner) {
return (
<div
className="absolute inset-0 overflow-y-auto p-4"
className="absolute inset-0 overflow-y-auto p-4 pb-0 pr-0"
ref={ref}
data-testid="messages-list"
>
......@@ -357,7 +357,7 @@ export const MessagesList = forwardRef<HTMLDivElement, MessagesListProps>(
}
return (
<div
className="absolute inset-0 overflow-y-auto p-4"
className="absolute inset-0 overflow-y-auto p-4 pb-0 pr-0"
ref={ref}
data-testid="messages-list"
>
......@@ -375,7 +375,7 @@ export const MessagesList = forwardRef<HTMLDivElement, MessagesListProps>(
if (isTestMode) {
return (
<div
className="absolute inset-0 p-4 overflow-y-auto"
className="absolute inset-0 p-4 pb-0 pr-0 overflow-y-auto"
ref={ref}
data-testid="messages-list"
>
......@@ -394,7 +394,7 @@ export const MessagesList = forwardRef<HTMLDivElement, MessagesListProps>(
return (
<div
className="absolute inset-0 overflow-y-auto p-4"
className="absolute inset-0 overflow-y-auto p-4 pb-0 mb-2 pr-0"
ref={ref}
data-testid="messages-list"
>
......
......@@ -67,7 +67,7 @@ function MiniSelectTrigger({
data-slot="select-trigger"
data-size={size}
className={cn(
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-7 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论