mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-23 19:05:16 +02:00
refactor: replace button elements with Button component for consistent styling across various UI components
This commit is contained in:
parent
88f9c3353c
commit
198c38b335
13 changed files with 234 additions and 180 deletions
|
|
@ -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" && (
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -1043,82 +1043,84 @@ 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"
|
||||||
onClick={() => setConnectorDialogOpen(true)}
|
size="sm"
|
||||||
className="flex min-w-0 flex-1 items-center gap-2 px-3 py-1.5 text-left"
|
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 text-muted-foreground" />
|
>
|
||||||
<span className="truncate text-xs text-muted-foreground">
|
<Unplug className="size-4 shrink-0" />
|
||||||
{connectorCount > 0 ? "Manage connectors" : "Connect your connectors"}
|
<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>
|
</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">
|
<AvatarGroup className="ml-auto shrink-0">
|
||||||
{connectorCount}
|
{connectorCount > 0 && connectors
|
||||||
</span>
|
? connectors.slice(0, isMobile ? 5 : 9).map((connector, i) => {
|
||||||
)}
|
const avatar = (
|
||||||
<AvatarGroup className="ml-auto shrink-0">
|
<Avatar
|
||||||
{connectorCount > 0 && connectors
|
key={connector.id}
|
||||||
? connectors.slice(0, isMobile ? 5 : 9).map((connector, i) => {
|
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 = (
|
const avatar = (
|
||||||
<Avatar
|
<Avatar
|
||||||
key={connector.id}
|
key={type}
|
||||||
className="size-6"
|
className="size-6"
|
||||||
style={{ zIndex: Math.max(9 - i, 1) }}
|
style={{ zIndex: SHOWCASE_CONNECTORS.length - i }}
|
||||||
>
|
>
|
||||||
<AvatarFallback className="bg-muted text-[10px]">
|
<AvatarFallback className="bg-muted text-[10px] text-muted-foreground">
|
||||||
{getConnectorIcon(connector.connector_type, "size-3.5")}
|
{getConnectorIcon(type, "size-3.5")}
|
||||||
</AvatarFallback>
|
</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
);
|
);
|
||||||
if (isMobile) return avatar;
|
if (isMobile) return avatar;
|
||||||
return (
|
return (
|
||||||
<Tooltip key={connector.id}>
|
<Tooltip key={type}>
|
||||||
<TooltipTrigger asChild>{avatar}</TooltipTrigger>
|
<TooltipTrigger asChild>{avatar}</TooltipTrigger>
|
||||||
<TooltipContent side="top" className="text-xs">
|
<TooltipContent side="top" className="text-xs">
|
||||||
{connector.name}
|
{label}
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
})
|
}
|
||||||
: (isMobile ? SHOWCASE_CONNECTORS.slice(0, 5) : SHOWCASE_CONNECTORS).map(
|
)}
|
||||||
({ type, label }, i) => {
|
</AvatarGroup>
|
||||||
const avatar = (
|
</Button>
|
||||||
<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>
|
|
||||||
|
|
||||||
{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,42 +1766,42 @@ 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"
|
||||||
onClick={() => gate("connect your data sources")}
|
size="sm"
|
||||||
className="flex min-w-0 flex-1 items-center gap-2 px-3 py-1.5 text-left"
|
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 text-muted-foreground" />
|
>
|
||||||
<span className="truncate text-xs text-muted-foreground">Connect your connectors</span>
|
<Unplug className="size-4 shrink-0" />
|
||||||
<AvatarGroup className="ml-auto shrink-0">
|
<span className="truncate">Connect your connectors</span>
|
||||||
{(isMobile ? SHOWCASE_CONNECTORS.slice(0, 5) : SHOWCASE_CONNECTORS).map(
|
<AvatarGroup className="ml-auto shrink-0">
|
||||||
({ type, label }, i) => {
|
{(isMobile ? SHOWCASE_CONNECTORS.slice(0, 5) : SHOWCASE_CONNECTORS).map(
|
||||||
const avatar = (
|
({ type, label }, i) => {
|
||||||
<Avatar
|
const avatar = (
|
||||||
key={type}
|
<Avatar
|
||||||
className="size-6"
|
key={type}
|
||||||
style={{ zIndex: SHOWCASE_CONNECTORS.length - i }}
|
className="size-6"
|
||||||
>
|
style={{ zIndex: SHOWCASE_CONNECTORS.length - i }}
|
||||||
<AvatarFallback className="bg-muted text-[10px]">
|
>
|
||||||
{getConnectorIcon(type, "size-3.5")}
|
<AvatarFallback className="bg-muted text-[10px] text-muted-foreground">
|
||||||
</AvatarFallback>
|
{getConnectorIcon(type, "size-3.5")}
|
||||||
</Avatar>
|
</AvatarFallback>
|
||||||
);
|
</Avatar>
|
||||||
if (isMobile) return avatar;
|
);
|
||||||
return (
|
if (isMobile) return avatar;
|
||||||
<Tooltip key={type}>
|
return (
|
||||||
<TooltipTrigger asChild>{avatar}</TooltipTrigger>
|
<Tooltip key={type}>
|
||||||
<TooltipContent side="top" className="text-xs">
|
<TooltipTrigger asChild>{avatar}</TooltipTrigger>
|
||||||
{label}
|
<TooltipContent side="top" className="text-xs">
|
||||||
</TooltipContent>
|
{label}
|
||||||
</Tooltip>
|
</TooltipContent>
|
||||||
);
|
</Tooltip>
|
||||||
}
|
);
|
||||||
)}
|
}
|
||||||
</AvatarGroup>
|
)}
|
||||||
</button>
|
</AvatarGroup>
|
||||||
</div>
|
</Button>
|
||||||
|
|
||||||
{/* 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.
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue