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

@ -7,6 +7,7 @@ import { useRouter } from "next/navigation";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { useState } from "react"; import { useState } from "react";
import { loginMutationAtom } from "@/atoms/auth/auth-mutation.atoms"; import { loginMutationAtom } from "@/atoms/auth/auth-mutation.atoms";
import { Button } from "@/components/ui/button";
import { Spinner } from "@/components/ui/spinner"; import { Spinner } from "@/components/ui/spinner";
import { getAuthErrorDetails, isNetworkError } from "@/lib/auth-errors"; import { getAuthErrorDetails, isNetworkError } from "@/lib/auth-errors";
import { AUTH_TYPE } from "@/lib/env-config"; import { AUTH_TYPE } from "@/lib/env-config";
@ -120,11 +121,13 @@ export function LocalLoginForm() {
<p className="text-sm font-semibold mb-1">{error.title}</p> <p className="text-sm font-semibold mb-1">{error.title}</p>
<p className="text-sm text-destructive">{error.message}</p> <p className="text-sm text-destructive">{error.message}</p>
</div> </div>
<button <Button
variant="ghost"
size="icon"
onClick={() => { onClick={() => {
setError({ title: null, message: null }); setError({ title: null, message: null });
}} }}
className="flex-shrink-0 text-destructive hover:text-destructive/90 transition-colors" className="size-6 flex-shrink-0 text-destructive hover:bg-transparent hover:text-destructive/90"
aria-label="Dismiss error" aria-label="Dismiss error"
type="button" type="button"
> >
@ -143,7 +146,7 @@ export function LocalLoginForm() {
<line x1="18" y1="6" x2="6" y2="18" /> <line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" /> <line x1="6" y1="6" x2="18" y2="18" />
</svg> </svg>
</button> </Button>
</div> </div>
</motion.div> </motion.div>
)} )}
@ -191,21 +194,23 @@ export function LocalLoginForm() {
}`} }`}
disabled={isLoggingIn} disabled={isLoggingIn}
/> />
<button <Button
type="button" type="button"
variant="ghost"
size="icon"
onClick={() => setShowPassword((prev) => !prev)} onClick={() => setShowPassword((prev) => !prev)}
className="absolute inset-y-0 right-0 flex items-center pr-3 text-muted-foreground hover:text-foreground" className="absolute inset-y-0 right-0 h-full w-10 text-muted-foreground hover:bg-transparent hover:text-foreground"
aria-label={showPassword ? t("hide_password") : t("show_password")} aria-label={showPassword ? t("hide_password") : t("show_password")}
> >
{showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />} {showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</button> </Button>
</div> </div>
</div> </div>
<button <Button
type="submit" type="submit"
disabled={isLoggingIn} disabled={isLoggingIn}
className="relative w-full rounded-md bg-primary px-4 py-1.5 md:py-2 text-primary-foreground shadow-sm hover:bg-primary/90 focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 transition-all text-sm md:text-base flex items-center justify-center gap-2" className="relative h-auto w-full px-4 py-1.5 text-sm md:py-2 md:text-base"
> >
<span className={isLoggingIn ? "invisible" : ""}>{t("sign_in")}</span> <span className={isLoggingIn ? "invisible" : ""}>{t("sign_in")}</span>
{isLoggingIn && ( {isLoggingIn && (
@ -213,7 +218,7 @@ export function LocalLoginForm() {
<Spinner size="sm" className="text-primary-foreground" /> <Spinner size="sm" className="text-primary-foreground" />
</span> </span>
)} )}
</button> </Button>
</form> </form>
{authType === "LOCAL" && ( {authType === "LOCAL" && (

View file

@ -6,6 +6,7 @@ import { useTranslations } from "next-intl";
import { Suspense, useEffect, useState } from "react"; import { Suspense, useEffect, useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { Logo } from "@/components/Logo"; import { Logo } from "@/components/Logo";
import { Button } from "@/components/ui/button";
import { useGlobalLoadingEffect } from "@/hooks/use-global-loading"; import { useGlobalLoadingEffect } from "@/hooks/use-global-loading";
import { getAuthErrorDetails, shouldRetry } from "@/lib/auth-errors"; import { getAuthErrorDetails, shouldRetry } from "@/lib/auth-errors";
import { setRedirectPath } from "@/lib/auth-utils"; import { setRedirectPath } from "@/lib/auth-utils";
@ -154,10 +155,12 @@ function LoginContent() {
<p className="text-sm font-semibold mb-1">{urlError.title}</p> <p className="text-sm font-semibold mb-1">{urlError.title}</p>
<p className="text-sm text-red-700 dark:text-red-300">{urlError.message}</p> <p className="text-sm text-red-700 dark:text-red-300">{urlError.message}</p>
</div> </div>
<button <Button
type="button" type="button"
variant="ghost"
size="icon"
onClick={() => setUrlError(null)} onClick={() => setUrlError(null)}
className="flex-shrink-0 text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-200 transition-colors" className="size-6 flex-shrink-0 text-red-500 hover:bg-transparent hover:text-red-700 dark:text-red-400 dark:hover:text-red-200"
aria-label="Dismiss error" aria-label="Dismiss error"
> >
<svg <svg
@ -175,7 +178,7 @@ function LoginContent() {
<line x1="18" y1="6" x2="6" y2="18" /> <line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" /> <line x1="6" y1="6" x2="18" y2="18" />
</svg> </svg>
</button> </Button>
</div> </div>
</motion.div> </motion.div>
)} )}

View file

@ -9,6 +9,7 @@ import { useEffect, useState } from "react";
import { type ExternalToast, toast } from "sonner"; import { type ExternalToast, toast } from "sonner";
import { registerMutationAtom } from "@/atoms/auth/auth-mutation.atoms"; import { registerMutationAtom } from "@/atoms/auth/auth-mutation.atoms";
import { Logo } from "@/components/Logo"; import { Logo } from "@/components/Logo";
import { Button } from "@/components/ui/button";
import { Spinner } from "@/components/ui/spinner"; import { Spinner } from "@/components/ui/spinner";
import { getAuthErrorDetails, isNetworkError, shouldRetry } from "@/lib/auth-errors"; import { getAuthErrorDetails, isNetworkError, shouldRetry } from "@/lib/auth-errors";
import { getBearerToken } from "@/lib/auth-utils"; import { getBearerToken } from "@/lib/auth-utils";
@ -199,11 +200,13 @@ export default function RegisterPage() {
<p className="text-sm font-semibold mb-1">{error.title}</p> <p className="text-sm font-semibold mb-1">{error.title}</p>
<p className="text-sm text-red-700 dark:text-red-300">{error.message}</p> <p className="text-sm text-red-700 dark:text-red-300">{error.message}</p>
</div> </div>
<button <Button
variant="ghost"
size="icon"
onClick={() => { onClick={() => {
setError({ title: null, message: null }); setError({ title: null, message: null });
}} }}
className="flex-shrink-0 text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-200 transition-colors" className="size-6 flex-shrink-0 text-red-500 hover:bg-transparent hover:text-red-700 dark:text-red-400 dark:hover:text-red-200"
aria-label="Dismiss error" aria-label="Dismiss error"
type="button" type="button"
> >
@ -222,7 +225,7 @@ export default function RegisterPage() {
<line x1="18" y1="6" x2="6" y2="18" /> <line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" /> <line x1="6" y1="6" x2="18" y2="18" />
</svg> </svg>
</button> </Button>
</div> </div>
</motion.div> </motion.div>
)} )}
@ -295,18 +298,18 @@ export default function RegisterPage() {
/> />
</div> </div>
<button <Button
type="submit" type="submit"
disabled={isRegistering} disabled={isRegistering}
className="relative w-full rounded-md bg-primary px-4 py-1.5 md:py-2 text-primary-foreground shadow-sm hover:bg-primary/90 focus:outline-none focus:ring-1 focus:ring-primary/40 disabled:cursor-not-allowed disabled:opacity-50 transition-all text-sm md:text-base flex items-center justify-center gap-2" className="relative h-auto w-full px-4 py-1.5 text-sm md:py-2 md:text-base"
> >
<span className={isRegistering ? "invisible" : ""}>{t("register")}</span> <span className={isRegistering ? "invisible" : ""}>{t("register")}</span>
{isRegistering && ( {isRegistering && (
<span className="absolute inset-0 flex items-center justify-center gap-2"> <span className="absolute inset-0 flex items-center justify-center gap-2">
<Spinner size="sm" className="text-white" /> <Spinner size="sm" className="text-primary-foreground" />
</span> </span>
)} )}
</button> </Button>
</form> </form>
<div className="mt-4 text-center text-sm"> <div className="mt-4 text-center text-sm">

View file

@ -4,6 +4,7 @@ import { motion } from "motion/react";
import { useState } from "react"; import { useState } from "react";
import { BuyPagesContent } from "@/components/settings/buy-pages-content"; import { BuyPagesContent } from "@/components/settings/buy-pages-content";
import { BuyTokensContent } from "@/components/settings/buy-tokens-content"; import { BuyTokensContent } from "@/components/settings/buy-tokens-content";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
const TABS = [ const TABS = [
@ -26,19 +27,21 @@ export default function BuyMorePage() {
> >
<div className="flex items-center justify-center rounded-lg border bg-muted/30 p-1"> <div className="flex items-center justify-center rounded-lg border bg-muted/30 p-1">
{TABS.map((tab) => ( {TABS.map((tab) => (
<button <Button
key={tab.id} key={tab.id}
type="button" type="button"
variant="ghost"
size="sm"
onClick={() => setActiveTab(tab.id)} onClick={() => setActiveTab(tab.id)}
className={cn( className={cn(
"flex-1 rounded-md px-3 py-1.5 text-sm font-medium transition-colors", "h-auto flex-1 px-3 py-1.5 text-sm",
activeTab === tab.id activeTab === tab.id
? "bg-background text-foreground shadow-sm" ? "bg-background text-foreground shadow-sm"
: "text-muted-foreground hover:text-accent-foreground" : "text-muted-foreground hover:text-accent-foreground"
)} )}
> >
{tab.label} {tab.label}
</button> </Button>
))} ))}
</div> </div>

View file

@ -44,6 +44,7 @@ import {
} from "@/components/assistant-ui/edit-message-dialog"; } from "@/components/assistant-ui/edit-message-dialog";
import { StepSeparatorDataUI } from "@/components/assistant-ui/step-separator"; import { StepSeparatorDataUI } from "@/components/assistant-ui/step-separator";
import { Thread } from "@/components/assistant-ui/thread"; import { Thread } from "@/components/assistant-ui/thread";
import { Button } from "@/components/ui/button";
import { import {
createTokenUsageStore, createTokenUsageStore,
type TokenUsageData, type TokenUsageData,
@ -76,8 +77,6 @@ import {
import { createStreamFlushHelpers } from "@/lib/chat/stream-flush"; import { createStreamFlushHelpers } from "@/lib/chat/stream-flush";
import { import {
consumeSseEvents, consumeSseEvents,
hasPersistableContent,
markInterruptsCompleted,
processSharedStreamEvent, processSharedStreamEvent,
} from "@/lib/chat/stream-pipeline"; } from "@/lib/chat/stream-pipeline";
import { import {
@ -88,7 +87,6 @@ import {
} from "@/lib/chat/stream-side-effects"; } from "@/lib/chat/stream-side-effects";
import { import {
addToolCall, addToolCall,
buildContentForPersistence,
buildContentForUI, buildContentForUI,
type ContentPartsState, type ContentPartsState,
type FrameBatchedUpdater, type FrameBatchedUpdater,
@ -1718,8 +1716,19 @@ export default function NewChatPage() {
} }
const byTcId = new Map<string, (typeof incoming)[number]>(); const byTcId = new Map<string, (typeof incoming)[number]>();
for (let i = 0; i < tcIds.length; i++) byTcId.set(tcIds[i], incoming[i]); const submittedDecisions: typeof incoming = [];
const submittedDecisions = tcIds.map((id) => byTcId.get(id)!); for (let i = 0; i < tcIds.length; i++) {
const tcId = tcIds[i];
const decision = incoming[i];
if (tcId === undefined || decision === undefined) {
toast.error(
`Cannot resume: ${incoming.length} decision(s) submitted for ${N} pending actions.`
);
return;
}
byTcId.set(tcId, decision);
submittedDecisions.push(decision);
}
setMessages((prev) => setMessages((prev) =>
prev.map((m) => { prev.map((m) => {
@ -2341,16 +2350,15 @@ export default function NewChatPage() {
return ( return (
<div className="flex h-full flex-col items-center justify-center gap-4"> <div className="flex h-full flex-col items-center justify-center gap-4">
<div className="text-destructive">Failed to load chat</div> <div className="text-destructive">Failed to load chat</div>
<button <Button
type="button" type="button"
onClick={() => { onClick={() => {
setIsInitializing(true); setIsInitializing(true);
initializeThread(); initializeThread();
}} }}
className="rounded-md bg-primary px-4 py-2 text-primary-foreground hover:bg-primary/90"
> >
Try Again Try Again
</button> </Button>
</div> </div>
); );
} }

View file

@ -482,13 +482,15 @@ function MemberRow({
{showActions ? ( {showActions ? (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<button <Button
type="button" type="button"
className="inline-flex items-center gap-1.5 text-sm text-muted-foreground hover:text-accent-foreground transition-colors" variant="ghost"
size="sm"
className="h-auto gap-1.5 px-0 py-0 text-sm text-muted-foreground hover:bg-transparent hover:text-accent-foreground"
> >
{roleName} {roleName}
<ChevronDown className="h-4 w-4" /> <ChevronDown className="h-4 w-4" />
</button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent <DropdownMenuContent
align="end" align="end"

View file

@ -6,6 +6,7 @@ import { useMemo } from "react";
import { agentFlagsAtom } from "@/atoms/agent/agent-flags-query.atom"; import { agentFlagsAtom } from "@/atoms/agent/agent-flags-query.atom";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import type { AgentFeatureFlags } from "@/lib/apis/agent-flags-api.service"; import type { AgentFeatureFlags } from "@/lib/apis/agent-flags-api.service";
@ -246,14 +247,16 @@ export function AgentStatusContent() {
<AlertTitle>Failed to load agent status</AlertTitle> <AlertTitle>Failed to load agent status</AlertTitle>
<AlertDescription className="flex items-center gap-2"> <AlertDescription className="flex items-center gap-2">
{error instanceof Error ? error.message : "Unknown error."} {error instanceof Error ? error.message : "Unknown error."}
<button <Button
type="button" type="button"
variant="outline"
size="sm"
onClick={() => refetch()} onClick={() => refetch()}
className="ml-auto inline-flex items-center gap-1 rounded-md border px-2 py-0.5 text-xs hover:bg-background" className="ml-auto h-auto gap-1 px-2 py-0.5 text-xs hover:bg-background"
> >
<RotateCcw className="size-3" /> <RotateCcw className="size-3" />
Retry Retry
</button> </Button>
</AlertDescription> </AlertDescription>
</Alert> </Alert>
); );

View file

@ -3,6 +3,7 @@
import { ExternalLink } from "lucide-react"; import { ExternalLink } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { useEffect, useMemo } from "react"; import { useEffect, useMemo } from "react";
import { Button } from "@/components/ui/button";
import { buildIssueUrl } from "@/lib/error-toast"; import { buildIssueUrl } from "@/lib/error-toast";
export default function DashboardError({ export default function DashboardError({
@ -39,13 +40,12 @@ export default function DashboardError({
)} )}
<div className="flex gap-2"> <div className="flex gap-2">
<button <Button
type="button" type="button"
onClick={reset} onClick={reset}
className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 transition-colors"
> >
Try again Try again
</button> </Button>
<Link <Link
href="/dashboard" href="/dashboard"
className="rounded-md border border-input bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground transition-colors" className="rounded-md border border-input bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground transition-colors"

View file

@ -2,6 +2,7 @@
import { ExternalLink } from "lucide-react"; import { ExternalLink } from "lucide-react";
import { useEffect, useMemo } from "react"; import { useEffect, useMemo } from "react";
import { Button } from "@/components/ui/button";
import { buildIssueUrl } from "@/lib/error-toast"; import { buildIssueUrl } from "@/lib/error-toast";
export default function ErrorPage({ export default function ErrorPage({
@ -37,13 +38,12 @@ export default function ErrorPage({
)} )}
<div className="flex gap-2"> <div className="flex gap-2">
<button <Button
type="button" type="button"
onClick={reset} onClick={reset}
className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 transition-colors"
> >
Try again Try again
</button> </Button>
<a <a
href={issueUrl} href={issueUrl}
target="_blank" target="_blank"

View file

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

View file

@ -9,9 +9,9 @@ import {
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuLabel, DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
@ -65,29 +65,30 @@ export function DesktopLocalTabContent({
return ( return (
<div className="flex min-h-0 flex-1 flex-col select-none"> <div className="flex min-h-0 flex-1 flex-col select-none">
<div className="mx-4 mt-4 mb-3"> <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 ? ( {localRootPaths.length > 0 ? (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<button <Button
type="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")} title={localRootPaths.join("\n")}
aria-label="Manage selected folders" aria-label="Manage selected folders"
> >
<Folder className="size-3 shrink-0 text-muted-foreground" /> <Folder className="size-3 shrink-0" />
<span className="truncate"> <span className="truncate">
{localRootPaths.length === 1 {localRootPaths.length === 1
? "1 folder selected" ? "1 folder selected"
: `${localRootPaths.length} folders selected`} : `${localRootPaths.length} folders selected`}
</span> </span>
</button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-56 select-none p-0.5"> <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"> <DropdownMenuLabel className="px-1.5 pt-1.5 pb-0.5 text-xs font-medium text-muted-foreground">
Selected folders Selected folders
</DropdownMenuLabel> </DropdownMenuLabel>
<DropdownMenuSeparator className="mx-1 my-0.5" />
{localRootPaths.map((rootPath) => ( {localRootPaths.map((rootPath) => (
<DropdownMenuItem <DropdownMenuItem
key={rootPath} key={rootPath}
@ -98,9 +99,11 @@ export function DesktopLocalTabContent({
<span className="min-w-0 flex-1 truncate"> <span className="min-w-0 flex-1 truncate">
{getFolderDisplayName(rootPath)} {getFolderDisplayName(rootPath)}
</span> </span>
<button <Button
type="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) => { onClick={(event) => {
event.stopPropagation(); event.stopPropagation();
void onRemoveFilesystemRoot(rootPath); void onRemoveFilesystemRoot(rootPath);
@ -108,10 +111,9 @@ export function DesktopLocalTabContent({
aria-label={`Remove ${getFolderDisplayName(rootPath)}`} aria-label={`Remove ${getFolderDisplayName(rootPath)}`}
> >
<X className="size-3" /> <X className="size-3" />
</button> </Button>
</DropdownMenuItem> </DropdownMenuItem>
))} ))}
<DropdownMenuSeparator className="mx-1 my-0.5" />
<DropdownMenuItem <DropdownMenuItem
variant="destructive" variant="destructive"
className="h-8 px-1.5 text-xs text-destructive focus:text-destructive" className="h-8 px-1.5 text-xs text-destructive focus:text-destructive"
@ -125,10 +127,10 @@ export function DesktopLocalTabContent({
</DropdownMenu> </DropdownMenu>
) : ( ) : (
<div <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" 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> <span className="truncate">No local folders selected</span>
</div> </div>
)} )}
@ -140,9 +142,11 @@ export function DesktopLocalTabContent({
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<span className="inline-flex"> <span className="inline-flex">
<button <Button
type="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={() => { onClick={() => {
void onPickFilesystemRoot(); void onPickFilesystemRoot();
}} }}
@ -150,7 +154,7 @@ export function DesktopLocalTabContent({
aria-label="Add folder" aria-label="Add folder"
> >
<FolderPlus className="size-3.5" /> <FolderPlus className="size-3.5" />
</button> </Button>
</span> </span>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent side="top" className="text-xs"> <TooltipContent side="top" className="text-xs">
@ -177,9 +181,11 @@ export function DesktopLocalTabContent({
aria-label="Search local files" aria-label="Search local files"
/> />
{Boolean(localSearch) && ( {Boolean(localSearch) && (
<button <Button
type="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" aria-label="Clear local search"
onClick={() => { onClick={() => {
setLocalSearch(""); setLocalSearch("");
@ -187,7 +193,7 @@ export function DesktopLocalTabContent({
}} }}
> >
<X size={13} strokeWidth={2} aria-hidden="true" /> <X size={13} strokeWidth={2} aria-hidden="true" />
</button> </Button>
)} )}
</div> </div>
</div> </div>

View file

@ -1043,14 +1043,15 @@ function AuthenticatedDocumentsSidebarBase({
const cloudContent = ( const cloudContent = (
<> <>
{/* Connected tools strip */} {/* 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
<button
type="button" type="button"
variant="ghost"
size="sm"
onClick={() => setConnectorDialogOpen(true)} onClick={() => setConnectorDialogOpen(true)}
className="flex min-w-0 flex-1 items-center gap-2 px-3 py-1.5 text-left" 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 text-muted-foreground" /> <Unplug className="size-4 shrink-0" />
<span className="truncate text-xs text-muted-foreground"> <span className="truncate">
{connectorCount > 0 ? "Manage connectors" : "Connect your connectors"} {connectorCount > 0 ? "Manage connectors" : "Connect your connectors"}
</span> </span>
{connectorCount > 0 && ( {connectorCount > 0 && (
@ -1067,7 +1068,7 @@ function AuthenticatedDocumentsSidebarBase({
className="size-6" className="size-6"
style={{ zIndex: Math.max(9 - i, 1) }} style={{ zIndex: Math.max(9 - i, 1) }}
> >
<AvatarFallback className="bg-muted text-[10px]"> <AvatarFallback className="bg-muted text-[10px] text-muted-foreground">
{getConnectorIcon(connector.connector_type, "size-3.5")} {getConnectorIcon(connector.connector_type, "size-3.5")}
</AvatarFallback> </AvatarFallback>
</Avatar> </Avatar>
@ -1090,7 +1091,7 @@ function AuthenticatedDocumentsSidebarBase({
className="size-6" className="size-6"
style={{ zIndex: SHOWCASE_CONNECTORS.length - i }} style={{ zIndex: SHOWCASE_CONNECTORS.length - i }}
> >
<AvatarFallback className="bg-muted text-[10px]"> <AvatarFallback className="bg-muted text-[10px] text-muted-foreground">
{getConnectorIcon(type, "size-3.5")} {getConnectorIcon(type, "size-3.5")}
</AvatarFallback> </AvatarFallback>
</Avatar> </Avatar>
@ -1107,18 +1108,19 @@ function AuthenticatedDocumentsSidebarBase({
} }
)} )}
</AvatarGroup> </AvatarGroup>
</button> </Button>
</div>
{isElectron && ( {isElectron && (
<button <Button
type="button" type="button"
variant="ghost"
size="sm"
onClick={handleWatchLocalFolder} 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" /> <FolderClock className="size-4 shrink-0" />
<span className="truncate text-xs text-muted-foreground">Watch local folder</span> <span className="truncate">Watch local folder</span>
</button> </Button>
)} )}
<div className="flex-1 min-h-0 pt-0 flex flex-col"> <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"> <div className="relative flex-1 min-h-0 overflow-auto">
{deletableSelectedIds.length > 0 && ( {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"> <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" type="button"
variant="destructive"
size="sm"
onClick={() => setBulkDeleteConfirmOpen(true)} 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} /> <Trash2 size={12} />
Delete {deletableSelectedIds.length}{" "} Delete {deletableSelectedIds.length}{" "}
{deletableSelectedIds.length === 1 ? "item" : "items"} {deletableSelectedIds.length === 1 ? "item" : "items"}
</button> </Button>
</div> </div>
)} )}
@ -1762,14 +1766,15 @@ function AnonymousDocumentsSidebar({
</div> </div>
{/* Connectors strip (gated) */} {/* 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
<button
type="button" type="button"
variant="ghost"
size="sm"
onClick={() => gate("connect your data sources")} onClick={() => gate("connect your data sources")}
className="flex min-w-0 flex-1 items-center gap-2 px-3 py-1.5 text-left" 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 text-muted-foreground" /> <Unplug className="size-4 shrink-0" />
<span className="truncate text-xs text-muted-foreground">Connect your connectors</span> <span className="truncate">Connect your connectors</span>
<AvatarGroup className="ml-auto shrink-0"> <AvatarGroup className="ml-auto shrink-0">
{(isMobile ? SHOWCASE_CONNECTORS.slice(0, 5) : SHOWCASE_CONNECTORS).map( {(isMobile ? SHOWCASE_CONNECTORS.slice(0, 5) : SHOWCASE_CONNECTORS).map(
({ type, label }, i) => { ({ type, label }, i) => {
@ -1779,7 +1784,7 @@ function AnonymousDocumentsSidebar({
className="size-6" className="size-6"
style={{ zIndex: SHOWCASE_CONNECTORS.length - i }} style={{ zIndex: SHOWCASE_CONNECTORS.length - i }}
> >
<AvatarFallback className="bg-muted text-[10px]"> <AvatarFallback className="bg-muted text-[10px] text-muted-foreground">
{getConnectorIcon(type, "size-3.5")} {getConnectorIcon(type, "size-3.5")}
</AvatarFallback> </AvatarFallback>
</Avatar> </Avatar>
@ -1796,8 +1801,7 @@ function AnonymousDocumentsSidebar({
} }
)} )}
</AvatarGroup> </AvatarGroup>
</button> </Button>
</div>
{/* Filters & upload */} {/* Filters & upload */}
<div className="flex-1 min-h-0 pt-0 flex flex-col"> <div className="flex-1 min-h-0 pt-0 flex flex-col">
@ -1849,18 +1853,19 @@ function AnonymousDocumentsSidebar({
{!hasDoc && ( {!hasDoc && (
<div className="px-4 py-8 text-center"> <div className="px-4 py-8 text-center">
<button <Button
type="button" type="button"
variant="ghost"
onClick={handleAnonUploadClick} onClick={handleAnonUploadClick}
disabled={isUploading} 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" : ""}`}> <span className={`flex items-center gap-2 ${isUploading ? "opacity-0" : ""}`}>
<Upload className="size-4" /> <Upload className="size-4" />
Upload a document Upload a document
</span> </span>
{isUploading && <Spinner size="sm" className="absolute" />} {isUploading && <Spinner size="sm" className="absolute" />}
</button> </Button>
<p className="mt-2 text-[11px] text-muted-foreground leading-relaxed"> <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+ Text, code, CSV, and HTML files only. Create an account for PDFs, images, and 30+
connectors. connectors.

View file

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