Polish chat input UX (#388)

This commit is contained in:
Will Chen
2025-06-11 14:39:55 -07:00
committed by GitHub
parent c1aa6803ce
commit b044acb61f
6 changed files with 110 additions and 66 deletions

View File

@@ -132,10 +132,7 @@ export class PageObject {
} }
async openContextFilesPicker() { async openContextFilesPicker() {
const contextButton = this.page.getByRole("button", { const contextButton = this.page.getByTestId("codebase-context-button");
name: "Context",
exact: true,
});
await contextButton.click(); await contextButton.click();
return new ContextFilesPickerDialog(this.page, async () => { return new ContextFilesPickerDialog(this.page, async () => {
await contextButton.click(); await contextButton.click();

View File

@@ -8,10 +8,17 @@ export function ChatInputControls({
showContextFilesPicker?: boolean; showContextFilesPicker?: boolean;
}) { }) {
return ( return (
<div className="pb-2 flex gap-2"> <div className="flex">
<ModelPicker /> <ModelPicker />
{showContextFilesPicker && <ContextFilesPicker />} <div className="w-2"></div>
<ProModeSelector /> <ProModeSelector />
<div className="w-1"></div>
{showContextFilesPicker && (
<>
<ContextFilesPicker />
<div className="w-0.5"></div>
</>
)}
</div> </div>
); );
} }

View File

@@ -6,7 +6,7 @@ import {
PopoverTrigger, PopoverTrigger,
} from "@/components/ui/popover"; } from "@/components/ui/popover";
import { FileCode, InfoIcon, Trash2 } from "lucide-react"; import { InfoIcon, Settings2, Trash2 } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
import { import {
Tooltip, Tooltip,
@@ -84,12 +84,22 @@ export function ContextFilesPicker() {
return ( return (
<Popover open={isOpen} onOpenChange={setIsOpen}> <Popover open={isOpen} onOpenChange={setIsOpen}>
<Tooltip>
<TooltipTrigger asChild>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button variant="ghost" className="gap-2"> <Button
<FileCode className="size-4" /> variant="ghost"
<span>Context</span> className="has-[>svg]:px-2"
size="sm"
data-testid="codebase-context-button"
>
<Settings2 className="size-4" />
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
</TooltipTrigger>
<TooltipContent>Codebase Context</TooltipContent>
</Tooltip>
<PopoverContent className="w-96" align="start"> <PopoverContent className="w-96" align="start">
<div className="relative space-y-4"> <div className="relative space-y-4">
<div> <div>

View File

@@ -123,13 +123,15 @@ export function ModelPicker() {
return ( return (
<DropdownMenu open={open} onOpenChange={setOpen}> <DropdownMenu open={open} onOpenChange={setOpen}>
<Tooltip>
<TooltipTrigger asChild>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
className="flex items-center gap-2 h-8" className="flex items-center gap-2 h-8 max-w-[160px] px-2"
> >
<span> <span className="truncate">
{modelDisplayName === "Auto" && ( {modelDisplayName === "Auto" && (
<> <>
<span className="text-xs text-muted-foreground"> <span className="text-xs text-muted-foreground">
@@ -141,7 +143,14 @@ export function ModelPicker() {
</span> </span>
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent className="w-64" align="start"> </TooltipTrigger>
<TooltipContent>{modelDisplayName}</TooltipContent>
</Tooltip>
<DropdownMenuContent
className="w-64"
align="start"
onCloseAutoFocus={(e) => e.preventDefault()}
>
<DropdownMenuLabel>Cloud Models</DropdownMenuLabel> <DropdownMenuLabel>Cloud Models</DropdownMenuLabel>
<DropdownMenuSeparator /> <DropdownMenuSeparator />

View File

@@ -40,7 +40,7 @@ export function ProModeSelector() {
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
className="flex items-center gap-2 h-8 border-primary/50 bg-primary/10 hover:bg-primary/20 font-medium shadow-sm shadow-primary/10 transition-all hover:shadow-md hover:shadow-primary/15" className="has-[>svg]:px-2 flex items-center gap-1.5 h-8 border-primary/50 hover:bg-primary/10 font-medium shadow-sm shadow-primary/10 transition-all hover:shadow-md hover:shadow-primary/15"
> >
<Sparkles className="h-4 w-4 text-primary" /> <Sparkles className="h-4 w-4 text-primary" />
<span className="text-primary font-medium">Pro</span> <span className="text-primary font-medium">Pro</span>

View File

@@ -1,5 +1,4 @@
import { import {
SendIcon,
StopCircleIcon, StopCircleIcon,
X, X,
ChevronDown, ChevronDown,
@@ -15,8 +14,9 @@ import {
Database, Database,
ChevronsUpDown, ChevronsUpDown,
ChevronsDownUp, ChevronsDownUp,
BarChart2,
Paperclip, Paperclip,
ChartColumnIncreasing,
SendHorizontalIcon,
} from "lucide-react"; } from "lucide-react";
import type React from "react"; import type React from "react";
import { useCallback, useEffect, useRef, useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
@@ -313,28 +313,10 @@ export function ChatInput({ chatId }: { chatId?: number }) {
disabled={isStreaming} disabled={isStreaming}
/> />
{/* File attachment button */}
<button
onClick={handleAttachmentClick}
className="px-2 py-2 mt-1 mr-1 hover:bg-(--background-darkest) text-(--sidebar-accent-fg) rounded-lg disabled:opacity-50"
disabled={isStreaming}
title="Attach files"
>
<Paperclip size={20} />
</button>
<input
type="file"
ref={fileInputRef}
onChange={handleFileChange}
className="hidden"
multiple
accept=".jpg,.jpeg,.png,.gif,.webp,.txt,.md,.js,.ts,.html,.css,.json,.csv"
/>
{isStreaming ? ( {isStreaming ? (
<button <button
onClick={handleCancel} onClick={handleCancel}
className="px-2 py-2 mt-1 mr-2 hover:bg-(--background-darkest) text-(--sidebar-accent-fg) rounded-lg" className="px-2 py-2 mt-1 mr-1 hover:bg-(--background-darkest) text-(--sidebar-accent-fg) rounded-lg"
title="Cancel generation" title="Cancel generation"
> >
<StopCircleIcon size={20} /> <StopCircleIcon size={20} />
@@ -343,23 +325,62 @@ export function ChatInput({ chatId }: { chatId?: number }) {
<button <button
onClick={handleSubmit} onClick={handleSubmit}
disabled={!inputValue.trim() && attachments.length === 0} disabled={!inputValue.trim() && attachments.length === 0}
className="px-2 py-2 mt-1 mr-2 hover:bg-(--background-darkest) text-(--sidebar-accent-fg) rounded-lg disabled:opacity-50" className="px-2 py-2 mt-1 mr-1 hover:bg-(--background-darkest) text-(--sidebar-accent-fg) rounded-lg disabled:opacity-50"
title="Send message" title="Send message"
> >
<SendIcon size={20} /> <SendHorizontalIcon size={20} />
</button> </button>
)} )}
</div> </div>
<div className="pl-2 pr-1 flex items-center justify-between"> <div className="pl-2 pr-1 flex items-center justify-between pb-2">
<div className="flex items-center">
<ChatInputControls showContextFilesPicker={true} /> <ChatInputControls showContextFilesPicker={true} />
<button {/* File attachment button */}
onClick={() => setShowTokenBar(!showTokenBar)} <TooltipProvider>
className="flex items-center px-2 py-1 text-xs text-muted-foreground hover:bg-muted rounded" <Tooltip>
title={showTokenBar ? "Hide token usage" : "Show token usage"} <TooltipTrigger asChild>
<Button
variant="ghost"
onClick={handleAttachmentClick}
disabled={isStreaming}
title="Attach files"
size="sm"
> >
<BarChart2 size={14} className="mr-1" /> <Paperclip size={20} />
{showTokenBar ? "Hide tokens" : "Tokens"} </Button>
</button> </TooltipTrigger>
<TooltipContent>Attach files</TooltipContent>
</Tooltip>
</TooltipProvider>
<input
type="file"
ref={fileInputRef}
onChange={handleFileChange}
className="hidden"
multiple
accept=".jpg,.jpeg,.png,.gif,.webp,.txt,.md,.js,.ts,.html,.css,.json,.csv"
/>
</div>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
onClick={() => setShowTokenBar(!showTokenBar)}
variant="ghost"
className={`has-[>svg]:px-2 ${
showTokenBar ? "text-purple-500 bg-purple-100" : ""
}`}
size="sm"
>
<ChartColumnIncreasing size={14} />
</Button>
</TooltipTrigger>
<TooltipContent>
{showTokenBar ? "Hide token usage" : "Show token usage"}
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div> </div>
{/* TokenBar is only displayed when showTokenBar is true */} {/* TokenBar is only displayed when showTokenBar is true */}
{showTokenBar && <TokenBar chatId={chatId} />} {showTokenBar && <TokenBar chatId={chatId} />}