refactor: replace button elements with Button component for consistent styling across various UI components

This commit is contained in:
Anish Sarkar 2026-05-14 13:30:20 +05:30
parent 88f9c3353c
commit 198c38b335
13 changed files with 234 additions and 180 deletions

View file

@ -2,6 +2,7 @@
import { ArrowUp, Loader2, Square } from "lucide-react";
import { useCallback, useEffect, useRef, useState } from "react";
import { Button } from "@/components/ui/button";
import type { AnonModel, AnonQuotaResponse } from "@/contracts/types/anonymous-chat.types";
import { anonymousChatApiService } from "@/lib/apis/anonymous-chat-api.service";
import { readSSEStream } from "@/lib/chat/streaming-state";
@ -28,14 +29,16 @@ export function AnonymousChat({ model }: AnonymousChatProps) {
const abortRef = useRef<AbortController | null>(null);
const messagesEndRef = useRef<HTMLDivElement>(null);
const textareaRef = useRef<HTMLTextAreaElement>(null);
const modelSlug = model.seo_slug ?? model.model_name;
useEffect(() => {
anonymousChatApiService.getQuota().then(setQuota).catch(console.error);
}, []);
useEffect(() => {
if (messages.length === 0) return;
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
}, [messages]);
}, [messages.length]);
const autoResizeTextarea = useCallback(() => {
const textarea = textareaRef.current;
@ -63,7 +66,7 @@ export function AnonymousChat({ model }: AnonymousChatProps) {
}
trackAnonymousChatMessageSent({
modelSlug: model.seo_slug,
modelSlug,
messageLength: trimmed.length,
surface: "free_model_page",
});
@ -84,7 +87,7 @@ export function AnonymousChat({ model }: AnonymousChatProps) {
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({
model_slug: model.seo_slug,
model_slug: modelSlug,
messages: chatHistory,
}),
signal: controller.signal,
@ -100,6 +103,7 @@ export function AnonymousChat({ model }: AnonymousChatProps) {
remaining: 0,
status: "exceeded",
warning_threshold: quota?.warning_threshold ?? 800000,
captcha_required: errorData.detail?.captcha_required ?? quota?.captcha_required ?? false,
});
setMessages((prev) => prev.filter((m) => m.id !== assistantId));
return;
@ -148,7 +152,7 @@ export function AnonymousChat({ model }: AnonymousChatProps) {
abortRef.current = null;
anonymousChatApiService.getQuota().then(setQuota).catch(console.error);
}
}, [input, isStreaming, messages, model.seo_slug, quota]);
}, [input, isStreaming, messages, modelSlug, quota]);
const handleCancel = useCallback(() => {
abortRef.current?.abort();
@ -258,22 +262,26 @@ export function AnonymousChat({ model }: AnonymousChatProps) {
)}
/>
{isStreaming ? (
<button
<Button
type="button"
variant="ghost"
size="icon"
onClick={handleCancel}
className="absolute right-2 bottom-2 flex h-8 w-8 items-center justify-center rounded-lg bg-foreground text-background transition-colors hover:opacity-80"
className="absolute right-2 bottom-2 size-8 bg-foreground text-background hover:bg-foreground hover:text-background hover:opacity-80"
>
<Square className="h-3.5 w-3.5" fill="currentColor" />
</button>
</Button>
) : (
<button
<Button
type="button"
variant="ghost"
size="icon"
onClick={handleSubmit}
disabled={!input.trim() || isExceeded}
className="absolute right-2 bottom-2 flex h-8 w-8 items-center justify-center rounded-lg bg-foreground text-background transition-colors hover:opacity-80 disabled:opacity-40 disabled:cursor-not-allowed"
className="absolute right-2 bottom-2 size-8 bg-foreground text-background hover:bg-foreground hover:text-background hover:opacity-80 disabled:opacity-40"
>
<ArrowUp className="h-4 w-4" />
</button>
</Button>
)}
</div>

View file

@ -9,9 +9,9 @@ import {
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Separator } from "@/components/ui/separator";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
@ -65,29 +65,30 @@ export function DesktopLocalTabContent({
return (
<div className="flex min-h-0 flex-1 flex-col select-none">
<div className="mx-4 mt-4 mb-3">
<div className="flex h-7 w-full items-stretch rounded-lg border-0 bg-muted text-[11px] text-muted-foreground hover:bg-accent hover:text-accent-foreground">
<div className="flex h-7 w-full items-stretch rounded-lg border-0 bg-muted text-[11px] text-muted-foreground">
{localRootPaths.length > 0 ? (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
<Button
type="button"
className="min-w-0 flex-1 flex items-center gap-1 rounded-l-lg px-2 text-left transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0"
variant="ghost"
size="sm"
className="min-w-0 flex-1 h-full justify-start gap-1 px-2 text-left text-[11px] text-muted-foreground"
title={localRootPaths.join("\n")}
aria-label="Manage selected folders"
>
<Folder className="size-3 shrink-0 text-muted-foreground" />
<Folder className="size-3 shrink-0" />
<span className="truncate">
{localRootPaths.length === 1
? "1 folder selected"
: `${localRootPaths.length} folders selected`}
</span>
</button>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-56 select-none p-0.5">
<DropdownMenuLabel className="px-1.5 pt-1.5 pb-0.5 text-xs font-medium text-muted-foreground">
Selected folders
</DropdownMenuLabel>
<DropdownMenuSeparator className="mx-1 my-0.5" />
{localRootPaths.map((rootPath) => (
<DropdownMenuItem
key={rootPath}
@ -98,9 +99,11 @@ export function DesktopLocalTabContent({
<span className="min-w-0 flex-1 truncate">
{getFolderDisplayName(rootPath)}
</span>
<button
<Button
type="button"
className="inline-flex size-5 items-center justify-center rounded text-muted-foreground transition-colors hover:text-accent-foreground"
variant="ghost"
size="icon"
className="size-5 text-muted-foreground hover:text-accent-foreground"
onClick={(event) => {
event.stopPropagation();
void onRemoveFilesystemRoot(rootPath);
@ -108,10 +111,9 @@ export function DesktopLocalTabContent({
aria-label={`Remove ${getFolderDisplayName(rootPath)}`}
>
<X className="size-3" />
</button>
</Button>
</DropdownMenuItem>
))}
<DropdownMenuSeparator className="mx-1 my-0.5" />
<DropdownMenuItem
variant="destructive"
className="h-8 px-1.5 text-xs text-destructive focus:text-destructive"
@ -125,10 +127,10 @@ export function DesktopLocalTabContent({
</DropdownMenu>
) : (
<div
className="min-w-0 flex-1 flex items-center gap-1 px-2"
className="min-w-0 flex-1 flex items-center gap-1 px-2 transition-colors hover:bg-accent hover:text-accent-foreground"
title="No local folders selected"
>
<Folder className="size-3 shrink-0 text-muted-foreground" />
<Folder className="size-3 shrink-0" />
<span className="truncate">No local folders selected</span>
</div>
)}
@ -140,9 +142,11 @@ export function DesktopLocalTabContent({
<Tooltip>
<TooltipTrigger asChild>
<span className="inline-flex">
<button
<Button
type="button"
className="flex w-8 items-center justify-center rounded-r-lg text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 disabled:opacity-50"
variant="ghost"
size="icon"
className="h-full w-8 text-muted-foreground hover:text-accent-foreground"
onClick={() => {
void onPickFilesystemRoot();
}}
@ -150,7 +154,7 @@ export function DesktopLocalTabContent({
aria-label="Add folder"
>
<FolderPlus className="size-3.5" />
</button>
</Button>
</span>
</TooltipTrigger>
<TooltipContent side="top" className="text-xs">
@ -177,9 +181,11 @@ export function DesktopLocalTabContent({
aria-label="Search local files"
/>
{Boolean(localSearch) && (
<button
<Button
type="button"
className="absolute inset-y-0 right-0 flex h-full w-8 items-center justify-center rounded-r-md text-muted-foreground hover:text-accent-foreground transition-colors"
variant="ghost"
size="icon"
className="absolute inset-y-0 right-0 h-full w-8 text-muted-foreground hover:text-accent-foreground"
aria-label="Clear local search"
onClick={() => {
setLocalSearch("");
@ -187,7 +193,7 @@ export function DesktopLocalTabContent({
}}
>
<X size={13} strokeWidth={2} aria-hidden="true" />
</button>
</Button>
)}
</div>
</div>

View file

@ -1043,82 +1043,84 @@ function AuthenticatedDocumentsSidebarBase({
const cloudContent = (
<>
{/* Connected tools strip */}
<div className="shrink-0 mx-4 mt-6 mb-2.5 flex select-none items-center gap-2 rounded-lg border-0 bg-muted transition-colors hover:bg-accent hover:text-accent-foreground">
<button
type="button"
onClick={() => setConnectorDialogOpen(true)}
className="flex min-w-0 flex-1 items-center gap-2 px-3 py-1.5 text-left"
>
<Unplug className="size-4 shrink-0 text-muted-foreground" />
<span className="truncate text-xs text-muted-foreground">
{connectorCount > 0 ? "Manage connectors" : "Connect your connectors"}
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => setConnectorDialogOpen(true)}
className="shrink-0 mx-4 mt-6 mb-2.5 h-auto select-none justify-start gap-2 bg-muted px-3 py-1.5 text-xs text-muted-foreground"
>
<Unplug className="size-4 shrink-0" />
<span className="truncate">
{connectorCount > 0 ? "Manage connectors" : "Connect your connectors"}
</span>
{connectorCount > 0 && (
<span className="shrink-0 rounded-full bg-muted-foreground/15 px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground">
{connectorCount}
</span>
{connectorCount > 0 && (
<span className="shrink-0 rounded-full bg-muted-foreground/15 px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground">
{connectorCount}
</span>
)}
<AvatarGroup className="ml-auto shrink-0">
{connectorCount > 0 && connectors
? connectors.slice(0, isMobile ? 5 : 9).map((connector, i) => {
)}
<AvatarGroup className="ml-auto shrink-0">
{connectorCount > 0 && connectors
? connectors.slice(0, isMobile ? 5 : 9).map((connector, i) => {
const avatar = (
<Avatar
key={connector.id}
className="size-6"
style={{ zIndex: Math.max(9 - i, 1) }}
>
<AvatarFallback className="bg-muted text-[10px] text-muted-foreground">
{getConnectorIcon(connector.connector_type, "size-3.5")}
</AvatarFallback>
</Avatar>
);
if (isMobile) return avatar;
return (
<Tooltip key={connector.id}>
<TooltipTrigger asChild>{avatar}</TooltipTrigger>
<TooltipContent side="top" className="text-xs">
{connector.name}
</TooltipContent>
</Tooltip>
);
})
: (isMobile ? SHOWCASE_CONNECTORS.slice(0, 5) : SHOWCASE_CONNECTORS).map(
({ type, label }, i) => {
const avatar = (
<Avatar
key={connector.id}
key={type}
className="size-6"
style={{ zIndex: Math.max(9 - i, 1) }}
style={{ zIndex: SHOWCASE_CONNECTORS.length - i }}
>
<AvatarFallback className="bg-muted text-[10px]">
{getConnectorIcon(connector.connector_type, "size-3.5")}
<AvatarFallback className="bg-muted text-[10px] text-muted-foreground">
{getConnectorIcon(type, "size-3.5")}
</AvatarFallback>
</Avatar>
);
if (isMobile) return avatar;
return (
<Tooltip key={connector.id}>
<Tooltip key={type}>
<TooltipTrigger asChild>{avatar}</TooltipTrigger>
<TooltipContent side="top" className="text-xs">
{connector.name}
{label}
</TooltipContent>
</Tooltip>
);
})
: (isMobile ? SHOWCASE_CONNECTORS.slice(0, 5) : SHOWCASE_CONNECTORS).map(
({ type, label }, i) => {
const avatar = (
<Avatar
key={type}
className="size-6"
style={{ zIndex: SHOWCASE_CONNECTORS.length - i }}
>
<AvatarFallback className="bg-muted text-[10px]">
{getConnectorIcon(type, "size-3.5")}
</AvatarFallback>
</Avatar>
);
if (isMobile) return avatar;
return (
<Tooltip key={type}>
<TooltipTrigger asChild>{avatar}</TooltipTrigger>
<TooltipContent side="top" className="text-xs">
{label}
</TooltipContent>
</Tooltip>
);
}
)}
</AvatarGroup>
</button>
</div>
}
)}
</AvatarGroup>
</Button>
{isElectron && (
<button
<Button
type="button"
variant="ghost"
size="sm"
onClick={handleWatchLocalFolder}
className="shrink-0 mx-4 mb-2.5 flex select-none items-center gap-2 rounded-lg border-0 bg-muted px-3 py-1.5 transition-colors hover:bg-accent hover:text-accent-foreground"
className="shrink-0 mx-4 mb-2.5 h-auto select-none justify-start gap-2 bg-muted px-3 py-1.5 text-xs text-muted-foreground"
>
<FolderClock className="size-4 shrink-0 text-muted-foreground" />
<span className="truncate text-xs text-muted-foreground">Watch local folder</span>
</button>
<FolderClock className="size-4 shrink-0" />
<span className="truncate">Watch local folder</span>
</Button>
)}
<div className="flex-1 min-h-0 pt-0 flex flex-col">
@ -1139,15 +1141,17 @@ function AuthenticatedDocumentsSidebarBase({
<div className="relative flex-1 min-h-0 overflow-auto">
{deletableSelectedIds.length > 0 && (
<div className="absolute inset-x-0 top-0 z-10 flex items-center justify-center px-4 py-1.5 animate-in fade-in duration-150 pointer-events-none">
<button
<Button
type="button"
variant="destructive"
size="sm"
onClick={() => setBulkDeleteConfirmOpen(true)}
className="pointer-events-auto flex items-center gap-1.5 px-3 py-1 rounded-md bg-destructive text-destructive-foreground shadow-lg text-xs font-medium hover:bg-destructive/90 transition-colors"
className="pointer-events-auto h-auto gap-1.5 px-3 py-1 text-xs shadow-lg"
>
<Trash2 size={12} />
Delete {deletableSelectedIds.length}{" "}
{deletableSelectedIds.length === 1 ? "item" : "items"}
</button>
</Button>
</div>
)}
@ -1762,42 +1766,42 @@ function AnonymousDocumentsSidebar({
</div>
{/* Connectors strip (gated) */}
<div className="shrink-0 mx-4 mt-6 mb-2.5 flex select-none items-center gap-2 rounded-lg border bg-muted/50 transition-colors hover:bg-accent hover:text-accent-foreground">
<button
type="button"
onClick={() => gate("connect your data sources")}
className="flex min-w-0 flex-1 items-center gap-2 px-3 py-1.5 text-left"
>
<Unplug className="size-4 shrink-0 text-muted-foreground" />
<span className="truncate text-xs text-muted-foreground">Connect your connectors</span>
<AvatarGroup className="ml-auto shrink-0">
{(isMobile ? SHOWCASE_CONNECTORS.slice(0, 5) : SHOWCASE_CONNECTORS).map(
({ type, label }, i) => {
const avatar = (
<Avatar
key={type}
className="size-6"
style={{ zIndex: SHOWCASE_CONNECTORS.length - i }}
>
<AvatarFallback className="bg-muted text-[10px]">
{getConnectorIcon(type, "size-3.5")}
</AvatarFallback>
</Avatar>
);
if (isMobile) return avatar;
return (
<Tooltip key={type}>
<TooltipTrigger asChild>{avatar}</TooltipTrigger>
<TooltipContent side="top" className="text-xs">
{label}
</TooltipContent>
</Tooltip>
);
}
)}
</AvatarGroup>
</button>
</div>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => gate("connect your data sources")}
className="shrink-0 mx-4 mt-6 mb-2.5 h-auto select-none justify-start gap-2 border bg-muted/50 px-3 py-1.5 text-xs text-muted-foreground"
>
<Unplug className="size-4 shrink-0" />
<span className="truncate">Connect your connectors</span>
<AvatarGroup className="ml-auto shrink-0">
{(isMobile ? SHOWCASE_CONNECTORS.slice(0, 5) : SHOWCASE_CONNECTORS).map(
({ type, label }, i) => {
const avatar = (
<Avatar
key={type}
className="size-6"
style={{ zIndex: SHOWCASE_CONNECTORS.length - i }}
>
<AvatarFallback className="bg-muted text-[10px] text-muted-foreground">
{getConnectorIcon(type, "size-3.5")}
</AvatarFallback>
</Avatar>
);
if (isMobile) return avatar;
return (
<Tooltip key={type}>
<TooltipTrigger asChild>{avatar}</TooltipTrigger>
<TooltipContent side="top" className="text-xs">
{label}
</TooltipContent>
</Tooltip>
);
}
)}
</AvatarGroup>
</Button>
{/* Filters & upload */}
<div className="flex-1 min-h-0 pt-0 flex flex-col">
@ -1849,18 +1853,19 @@ function AnonymousDocumentsSidebar({
{!hasDoc && (
<div className="px-4 py-8 text-center">
<button
<Button
type="button"
variant="ghost"
onClick={handleAnonUploadClick}
disabled={isUploading}
className="relative flex w-full items-center justify-center rounded-lg border-2 border-dashed border-primary/30 px-4 py-6 text-sm text-primary transition-colors hover:border-primary/60 hover:bg-primary/5 cursor-pointer disabled:opacity-50 disabled:pointer-events-none"
className="relative h-auto w-full border-2 border-dashed border-primary/30 px-4 py-6 text-sm text-primary hover:border-primary/60 hover:bg-primary/5 hover:text-primary cursor-pointer"
>
<span className={`flex items-center gap-2 ${isUploading ? "opacity-0" : ""}`}>
<Upload className="size-4" />
Upload a document
</span>
{isUploading && <Spinner size="sm" className="absolute" />}
</button>
</Button>
<p className="mt-2 text-[11px] text-muted-foreground leading-relaxed">
Text, code, CSV, and HTML files only. Create an account for PDFs, images, and 30+
connectors.

View file

@ -9,6 +9,7 @@ import { createPortal } from "react-dom";
import { connectorsAtom } from "@/atoms/connectors/connector-query.atoms";
import { activeSearchSpaceIdAtom } from "@/atoms/search-spaces/search-space-query.atoms";
import { currentUserAtom } from "@/atoms/user/user-query.atoms";
import { Button } from "@/components/ui/button";
import { useIsMobile } from "@/hooks/use-mobile";
import { useZeroDocumentTypeCounts } from "@/hooks/use-zero-document-type-counts";
import { fetchThreads } from "@/lib/chat/thread-persistence";
@ -322,39 +323,45 @@ function TourTooltip({
{/* Navigation buttons */}
<div className="flex items-center gap-3">
{!isFirstStep && (
<button
<Button
type="button"
variant="ghost"
size="sm"
onClick={(e) => {
e.stopPropagation();
onPrev();
}}
className="text-sm font-medium text-muted-foreground transition-opacity hover:opacity-80"
className="h-auto px-0 py-0 text-sm text-muted-foreground hover:bg-transparent hover:text-muted-foreground hover:opacity-80"
>
Back
</button>
</Button>
)}
{isFirstStep && (
<button
<Button
type="button"
variant="ghost"
size="sm"
onClick={(e) => {
e.stopPropagation();
onSkip();
}}
className="text-sm font-medium text-muted-foreground transition-opacity hover:opacity-80"
className="h-auto px-0 py-0 text-sm text-muted-foreground hover:bg-transparent hover:text-muted-foreground hover:opacity-80"
>
Skip
</button>
</Button>
)}
<button
<Button
type="button"
variant="ghost"
size="sm"
onClick={(e) => {
e.stopPropagation();
onNext();
}}
className="text-sm font-medium text-foreground transition-opacity hover:opacity-80"
className="h-auto px-0 py-0 text-sm text-foreground hover:bg-transparent hover:text-foreground hover:opacity-80"
>
{isLastStep ? "Done" : "Next"}
</button>
</Button>
</div>
</div>
</div>
@ -714,9 +721,10 @@ export function OnboardingTour() {
`}</style>
<div className="fixed inset-0 z-[99995]">
{/* Clickable backdrop to close */}
<button
<Button
type="button"
className="fixed inset-0 w-full h-full bg-transparent border-0 cursor-default"
variant="ghost"
className="fixed inset-0 h-full w-full rounded-none border-0 bg-transparent p-0 hover:bg-transparent cursor-default"
onClick={handleOverlayClick}
aria-label="Close tour"
/>