refactor: enhance user settings components with updated icons, improved loading states, and consistent alert structures

This commit is contained in:
Anish Sarkar 2026-05-19 11:11:14 +05:30
parent c8f0f7cb1b
commit 49e1395299
16 changed files with 703 additions and 649 deletions

View file

@ -22,7 +22,7 @@ export function AnnouncementsDialog() {
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="select-none max-w-[900px] w-[95vw] md:w-[90vw] h-[90vh] md:h-[80vh] max-h-[640px] flex flex-col p-0 gap-0 overflow-hidden [--card:var(--popover)]">
<DialogContent className="select-none max-w-[900px] w-[95vw] md:w-[90vw] h-[90vh] md:h-[80vh] max-h-[640px] flex flex-col p-0 gap-0 overflow-hidden bg-popover text-popover-foreground">
<DialogTitle className="sr-only">What's New</DialogTitle>
<div className="flex flex-1 flex-col overflow-hidden min-w-0">

View file

@ -1,7 +1,7 @@
"use client";
import { useAtomValue } from "jotai";
import { Plus, Zap } from "lucide-react";
import { Plus, WandSparkles } from "lucide-react";
import { useParams, useRouter } from "next/navigation";
import {
forwardRef,
@ -173,7 +173,7 @@ export const PromptPicker = forwardRef<PromptPickerRef, PromptPickerProps>(funct
)}
>
<span className="shrink-0 text-muted-foreground">
<Zap className="size-4" />
<WandSparkles className="size-4" />
</span>
<span className="flex-1 text-sm truncate">{action.name}</span>
</Button>

View file

@ -13,6 +13,7 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Separator } from "@/components/ui/separator";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import type { PublicChatSnapshotDetail } from "@/contracts/types/chat-threads.types";
import { useMediaQuery } from "@/hooks/use-media-query";
@ -60,8 +61,8 @@ export function PublicChatSnapshotRow({
const member = snapshot.created_by_user_id ? memberMap.get(snapshot.created_by_user_id) : null;
return (
<Card className="group relative overflow-hidden transition-all duration-200 border-border/60 hover:shadow-md h-full">
<CardContent className="p-4 flex flex-col gap-3 h-full">
<Card className="group relative overflow-hidden transition-all duration-200 border-accent bg-accent/20 hover:shadow-md h-full">
<CardContent className="p-4 flex flex-col gap-3 h-full min-h-32">
{/* Header: Title + Actions */}
<div className="relative flex items-center">
<h4
@ -122,33 +123,38 @@ export function PublicChatSnapshotRow({
</div>
{/* Footer: Date + Creator */}
<div className="flex items-center gap-2 pt-2 border-t border-border/40 mt-auto">
<span className="text-[11px] text-muted-foreground/60">{formattedDate}</span>
{member && (
<>
<Dot className="h-4 w-4 text-muted-foreground/30" />
<TooltipProvider>
<Tooltip open={isDesktop ? undefined : false}>
<TooltipTrigger asChild>
<div className="flex items-center gap-1.5 cursor-default">
<Avatar className="size-4.5 shrink-0">
{member.avatarUrl && (
<AvatarImage src={member.avatarUrl} alt={member.name} />
)}
<AvatarFallback className="text-[9px]">
{getInitials(member.name)}
</AvatarFallback>
</Avatar>
<span className="text-[11px] text-muted-foreground/60 truncate max-w-[120px]">
{member.name}
</span>
</div>
</TooltipTrigger>
<TooltipContent side="bottom">{member.email || member.name}</TooltipContent>
</Tooltip>
</TooltipProvider>
</>
)}
<div className="mt-auto space-y-2">
<Separator className="bg-accent" />
<div className="flex items-center">
<span className="shrink-0 text-[11px] text-muted-foreground/60 whitespace-nowrap">
{formattedDate}
</span>
{member && (
<>
<Dot className="h-4 w-4 text-muted-foreground/30 shrink-0" />
<TooltipProvider>
<Tooltip open={isDesktop ? undefined : false}>
<TooltipTrigger asChild>
<div className="min-w-0 flex items-center gap-1.5 cursor-default">
<Avatar className="size-4.5 shrink-0">
{member.avatarUrl && (
<AvatarImage src={member.avatarUrl} alt={member.name} />
)}
<AvatarFallback className="text-[9px]">
{getInitials(member.name)}
</AvatarFallback>
</Avatar>
<span className="text-[11px] text-muted-foreground/60 truncate">
{member.name}
</span>
</div>
</TooltipTrigger>
<TooltipContent side="bottom">{member.email || member.name}</TooltipContent>
</Tooltip>
</TooltipProvider>
</>
)}
</div>
</div>
</CardContent>
</Card>

View file

@ -81,30 +81,26 @@ export function PublicChatSnapshotsManager({
if (isLoading) {
return (
<div className="space-y-4 md:space-y-5">
{/* Info alert skeleton */}
<Skeleton className="h-12 w-full rounded-lg" />
<Alert>
<Info />
<AlertDescription>
<div className="flex min-h-[1.625em] items-center">
<Skeleton className="h-4 w-60 bg-accent-foreground/15" />
</div>
</AlertDescription>
</Alert>
{/* Cards grid skeleton */}
<div className="grid gap-3 grid-cols-1 sm:grid-cols-2">
{["skeleton-a", "skeleton-b", "skeleton-c"].map((key) => (
<Card key={key} className="border-border/60">
<CardContent className="p-4 flex flex-col gap-3">
{/* Header: Title */}
<div className="flex items-start justify-between gap-2">
<Skeleton className="h-4 w-36 md:w-44" />
</div>
{/* Message count badge */}
<div className="flex items-center gap-1.5">
<Skeleton className="h-5 w-24 rounded-full" />
</div>
{/* URL skeleton */}
<Skeleton className="h-3 w-full rounded" />
{/* Footer: Date + Creator */}
<div className="flex items-center gap-2 pt-2 border-t border-border/40">
<Skeleton className="h-3 w-20" />
<Skeleton className="h-4 w-4 rounded-full" />
<Skeleton className="h-3 w-16" />
</div>
<Card
key={key}
className="group relative overflow-hidden transition-all duration-200 border-accent bg-accent/20 hover:shadow-md h-full"
>
<CardContent className="p-4 flex flex-col gap-3 h-full min-h-32">
<Skeleton className="h-4 w-32 md:w-40 bg-accent" />
<Skeleton className="h-3 w-full bg-accent" />
<Skeleton className="h-3 w-24 md:w-28 bg-accent mt-auto" />
</CardContent>
</Card>
))}

View file

@ -25,6 +25,7 @@ import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { Separator } from "@/components/ui/separator";
import { Skeleton } from "@/components/ui/skeleton";
import { Spinner } from "@/components/ui/spinner";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
@ -130,7 +131,7 @@ export function AgentModelManager({ searchSpaceId }: AgentModelManagerProps) {
<Button
variant="outline"
onClick={openNewDialog}
className="gap-2 bg-white text-black hover:bg-accent hover:text-accent-foreground dark:bg-white dark:text-black"
className="gap-2 border-transparent bg-white text-[#1f1f1f] font-medium hover:bg-zinc-100 hover:text-[#1f1f1f] dark:border-transparent dark:bg-white dark:text-[#1f1f1f]"
>
Add Model
</Button>
@ -182,16 +183,22 @@ export function AgentModelManager({ searchSpaceId }: AgentModelManagerProps) {
)}
{/* Global Configs Info */}
{globalConfigs.length > 0 && (
{(isLoading || globalConfigs.length > 0) && (
<Alert>
<Info />
<AlertDescription>
<p>
<span className="font-medium">
{globalConfigs.length} global {globalConfigs.length === 1 ? "model" : "models"}
</span>{" "}
available from your administrator.
</p>
{isLoading ? (
<div className="flex min-h-[1.625em] items-center">
<Skeleton className="h-4 w-60 bg-accent-foreground/15" />
</div>
) : (
<p>
<span className="font-medium">
{globalConfigs.length} global {globalConfigs.length === 1 ? "model" : "models"}
</span>{" "}
available from your administrator.
</p>
)}
</AlertDescription>
</Alert>
)}
@ -200,29 +207,11 @@ export function AgentModelManager({ searchSpaceId }: AgentModelManagerProps) {
{isLoading && (
<div className="grid gap-3 grid-cols-1 sm:grid-cols-2 xl:grid-cols-3">
{["skeleton-a", "skeleton-b", "skeleton-c"].map((key) => (
<Card key={key} className="border-border/60">
<CardContent className="p-4 flex flex-col gap-3">
{/* Header: Icon + Name */}
<div className="flex items-start gap-2.5">
<Skeleton className="size-4 rounded-full shrink-0 mt-0.5" />
<div className="space-y-1.5 flex-1 min-w-0">
<Skeleton className="h-4 w-28 md:w-32" />
<Skeleton className="h-3 w-40 md:w-48" />
</div>
</div>
{/* Feature badges */}
<div className="flex items-center gap-1.5">
<Skeleton className="h-5 w-20 rounded-full" />
</div>
{/* Footer */}
<div className="flex items-center pt-2 border-t border-border/40">
<Skeleton className="h-3 w-20 flex-1" />
<Skeleton className="h-3 w-3 rounded-full shrink-0 mx-1" />
<div className="flex-1 flex items-center justify-end gap-1.5">
<Skeleton className="h-4 w-4 rounded-full" />
<Skeleton className="h-3 w-16" />
</div>
</div>
<Card key={key} className="border-accent bg-accent/20">
<CardContent className="p-4 flex flex-col gap-3 min-h-32">
<Skeleton className="h-4 w-32 md:w-40 bg-accent" />
<Skeleton className="h-3 w-full bg-accent" />
<Skeleton className="h-3 w-24 md:w-28 bg-accent mt-auto" />
</CardContent>
</Card>
))}
@ -252,7 +241,7 @@ export function AgentModelManager({ searchSpaceId }: AgentModelManagerProps) {
return (
<div key={config.id}>
<Card className="group relative overflow-hidden transition-all duration-200 border-border/60 hover:shadow-md h-full">
<Card className="group relative overflow-hidden transition-all duration-200 border-accent bg-accent/20 hover:shadow-md h-full">
<CardContent className="p-4 flex flex-col gap-3 h-full">
{/* Header: Icon + Name + Actions */}
<div className="flex items-center justify-between gap-2">
@ -334,41 +323,44 @@ export function AgentModelManager({ searchSpaceId }: AgentModelManagerProps) {
</div>
{/* Footer: Date + Creator */}
<div className="flex items-center pt-2 border-t border-border/40 mt-auto">
<span className="shrink-0 text-[11px] text-muted-foreground/60 whitespace-nowrap">
{new Date(config.created_at).toLocaleDateString(undefined, {
year: "numeric",
month: "short",
day: "numeric",
})}
</span>
{member && (
<>
<Dot className="h-4 w-4 text-muted-foreground/30 shrink-0" />
<TooltipProvider>
<Tooltip open={isDesktop ? undefined : false}>
<TooltipTrigger asChild>
<div className="min-w-0 flex items-center gap-1.5 cursor-default">
<Avatar className="size-4.5 shrink-0">
{member.avatarUrl && (
<AvatarImage src={member.avatarUrl} alt={member.name} />
)}
<AvatarFallback className="text-[9px]">
{getInitials(member.name)}
</AvatarFallback>
</Avatar>
<span className="text-[11px] text-muted-foreground/60 truncate">
{member.name}
</span>
</div>
</TooltipTrigger>
<TooltipContent side="bottom">
{member.email || member.name}
</TooltipContent>
</Tooltip>
</TooltipProvider>
</>
)}
<div className="mt-auto space-y-2">
<Separator className="bg-accent" />
<div className="flex items-center">
<span className="shrink-0 text-[11px] text-muted-foreground/60 whitespace-nowrap">
{new Date(config.created_at).toLocaleDateString(undefined, {
year: "numeric",
month: "short",
day: "numeric",
})}
</span>
{member && (
<>
<Dot className="h-4 w-4 text-muted-foreground/30 shrink-0" />
<TooltipProvider>
<Tooltip open={isDesktop ? undefined : false}>
<TooltipTrigger asChild>
<div className="min-w-0 flex items-center gap-1.5 cursor-default">
<Avatar className="size-4.5 shrink-0">
{member.avatarUrl && (
<AvatarImage src={member.avatarUrl} alt={member.name} />
)}
<AvatarFallback className="text-[9px]">
{getInitials(member.name)}
</AvatarFallback>
</Avatar>
<span className="text-[11px] text-muted-foreground/60 truncate">
{member.name}
</span>
</div>
</TooltipTrigger>
<TooltipContent side="bottom">
{member.email || member.name}
</TooltipContent>
</Tooltip>
</TooltipProvider>
</>
)}
</div>
</div>
</CardContent>
</Card>

View file

@ -25,6 +25,7 @@ import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { Separator } from "@/components/ui/separator";
import { Skeleton } from "@/components/ui/skeleton";
import { Spinner } from "@/components/ui/spinner";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
@ -133,7 +134,7 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
<Button
variant="outline"
onClick={openNewDialog}
className="gap-2 bg-white text-black hover:bg-accent hover:text-accent-foreground dark:bg-white dark:text-black"
className="gap-2 border-transparent bg-white text-[#1f1f1f] font-medium hover:bg-zinc-100 hover:text-[#1f1f1f] dark:border-transparent dark:bg-white dark:text-[#1f1f1f]"
>
Add Image Model
</Button>
@ -183,37 +184,45 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
)}
{/* Global info */}
{globalConfigs.filter((g) => !("is_auto_mode" in g && g.is_auto_mode)).length > 0 && (
{(isLoading ||
globalConfigs.filter((g) => !("is_auto_mode" in g && g.is_auto_mode)).length > 0) && (
<Alert>
<Info />
<AlertDescription>
<p>
<span className="font-medium">
{globalConfigs.filter((g) => !("is_auto_mode" in g && g.is_auto_mode)).length}{" "}
global image{" "}
{globalConfigs.filter((g) => !("is_auto_mode" in g && g.is_auto_mode)).length === 1
? "model"
: "models"}
</span>{" "}
available from your administrator. {(() => {
const nonAuto = globalConfigs.filter(
(g) => !("is_auto_mode" in g && g.is_auto_mode)
);
const premium = nonAuto.filter(
(g) =>
"billing_tier" in g &&
(g as { billing_tier?: string }).billing_tier === "premium"
).length;
const free = nonAuto.length - premium;
if (premium > 0 && free > 0) {
return `${premium} premium, ${free} free.`;
}
if (premium > 0) {
return `All ${premium} premium — debits your shared credit pool.`;
}
return `All ${free} free.`;
})()}
</p>
{isLoading ? (
<div className="flex min-h-[1.625em] items-center">
<Skeleton className="h-4 w-60 bg-accent-foreground/15" />
</div>
) : (
<p>
<span className="font-medium">
{globalConfigs.filter((g) => !("is_auto_mode" in g && g.is_auto_mode)).length}{" "}
global image{" "}
{globalConfigs.filter((g) => !("is_auto_mode" in g && g.is_auto_mode)).length ===
1
? "model"
: "models"}
</span>{" "}
available from your administrator. {(() => {
const nonAuto = globalConfigs.filter(
(g) => !("is_auto_mode" in g && g.is_auto_mode)
);
const premium = nonAuto.filter(
(g) =>
"billing_tier" in g &&
(g as { billing_tier?: string }).billing_tier === "premium"
).length;
const free = nonAuto.length - premium;
if (premium > 0 && free > 0) {
return `${premium} premium, ${free} free.`;
}
if (premium > 0) {
return `All ${premium} premium — debits your shared credit pool.`;
}
return `All ${free} free.`;
})()}
</p>
)}
</AlertDescription>
</Alert>
)}
@ -225,9 +234,6 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
{!isLoading &&
globalConfigs.filter((g) => !("is_auto_mode" in g && g.is_auto_mode)).length > 0 && (
<div className="space-y-3">
<h3 className="text-xs md:text-sm font-semibold text-muted-foreground">
Global Image Models
</h3>
<div className="grid gap-3 grid-cols-1 sm:grid-cols-2 xl:grid-cols-3">
{globalConfigs
.filter((g) => !("is_auto_mode" in g && g.is_auto_mode))
@ -241,7 +247,7 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
return (
<Card
key={cfg.id}
className="border-border/60 bg-muted/20 overflow-hidden h-full"
className="group relative overflow-hidden transition-all duration-200 border-accent bg-accent/20 hover:shadow-md h-full"
>
<CardContent className="p-4 flex flex-col gap-3 h-full">
<div className="flex items-center gap-2 min-w-0">
@ -274,10 +280,13 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
{cfg.description}
</p>
)}
<div className="flex items-center pt-2 border-t border-border/40 mt-auto">
<span className="text-[11px] text-muted-foreground/60 truncate">
{cfg.model_name}
</span>
<div className="mt-auto space-y-2">
<Separator className="bg-accent" />
<div className="flex items-center">
<span className="text-[11px] text-muted-foreground/60 truncate">
{cfg.model_name}
</span>
</div>
</div>
</CardContent>
</Card>
@ -291,30 +300,13 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
{isLoading && (
<div className="space-y-4 md:space-y-6">
<div className="space-y-4">
<div className="flex items-center justify-between">
<Skeleton className="h-6 md:h-7 w-40 md:w-48" />
<Skeleton className="h-8 md:h-9 w-32 md:w-36 rounded-md" />
</div>
<div className="grid gap-3 grid-cols-1 sm:grid-cols-2 xl:grid-cols-3">
{["skeleton-a", "skeleton-b", "skeleton-c"].map((key) => (
<Card key={key} className="border-border/60">
<CardContent className="p-4 flex flex-col gap-3">
<div className="flex items-center gap-2.5">
<Skeleton className="size-4 rounded-full shrink-0" />
<div className="space-y-1.5 flex-1 min-w-0">
<Skeleton className="h-4 w-28 md:w-32" />
<Skeleton className="h-3 w-40 md:w-48" />
</div>
</div>
<div className="flex items-center pt-2 border-t border-border/40">
<Skeleton className="h-3 w-20 flex-1" />
<Skeleton className="h-3 w-3 rounded-full shrink-0 mx-1" />
<div className="flex-1 flex items-center justify-end gap-1.5">
<Skeleton className="h-4 w-4 rounded-full" />
<Skeleton className="h-3 w-16" />
</div>
</div>
<Card key={key} className="border-accent bg-accent/20">
<CardContent className="p-4 flex flex-col gap-3 min-h-32">
<Skeleton className="h-4 w-32 md:w-40 bg-accent" />
<Skeleton className="h-3 w-full bg-accent" />
<Skeleton className="h-3 w-24 md:w-28 bg-accent mt-auto" />
</CardContent>
</Card>
))}
@ -344,7 +336,7 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
return (
<div key={config.id}>
<Card className="group relative overflow-hidden transition-all duration-200 border-border/60 hover:shadow-md h-full">
<Card className="group relative overflow-hidden transition-all duration-200 border-accent bg-accent/20 hover:shadow-md h-full">
<CardContent className="p-4 flex flex-col gap-3 h-full">
{/* Header: Icon + Name + Actions */}
<div className="flex items-center justify-between gap-2">
@ -404,41 +396,44 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
</div>
{/* Footer: Date + Creator */}
<div className="flex items-center pt-2 border-t border-border/40 mt-auto">
<span className="shrink-0 text-[11px] text-muted-foreground/60 whitespace-nowrap">
{new Date(config.created_at).toLocaleDateString(undefined, {
year: "numeric",
month: "short",
day: "numeric",
})}
</span>
{member && (
<>
<Dot className="h-4 w-4 text-muted-foreground/30 shrink-0" />
<TooltipProvider>
<Tooltip open={isDesktop ? undefined : false}>
<TooltipTrigger asChild>
<div className="min-w-0 flex items-center gap-1.5 cursor-default">
<Avatar className="size-4.5 shrink-0">
{member.avatarUrl && (
<AvatarImage src={member.avatarUrl} alt={member.name} />
)}
<AvatarFallback className="text-[9px]">
{getInitials(member.name)}
</AvatarFallback>
</Avatar>
<span className="text-[11px] text-muted-foreground/60 truncate">
{member.name}
</span>
</div>
</TooltipTrigger>
<TooltipContent side="bottom">
{member.email || member.name}
</TooltipContent>
</Tooltip>
</TooltipProvider>
</>
)}
<div className="mt-auto space-y-2">
<Separator className="bg-accent" />
<div className="flex items-center">
<span className="shrink-0 text-[11px] text-muted-foreground/60 whitespace-nowrap">
{new Date(config.created_at).toLocaleDateString(undefined, {
year: "numeric",
month: "short",
day: "numeric",
})}
</span>
{member && (
<>
<Dot className="h-4 w-4 text-muted-foreground/30 shrink-0" />
<TooltipProvider>
<Tooltip open={isDesktop ? undefined : false}>
<TooltipTrigger asChild>
<div className="min-w-0 flex items-center gap-1.5 cursor-default">
<Avatar className="size-4.5 shrink-0">
{member.avatarUrl && (
<AvatarImage src={member.avatarUrl} alt={member.name} />
)}
<AvatarFallback className="text-[9px]">
{getInitials(member.name)}
</AvatarFallback>
</Avatar>
<span className="text-[11px] text-muted-foreground/60 truncate">
{member.name}
</span>
</div>
</TooltipTrigger>
<TooltipContent side="bottom">
{member.email || member.name}
</TooltipContent>
</Tooltip>
</TooltipProvider>
</>
)}
</div>
</div>
</CardContent>
</Card>

View file

@ -236,35 +236,11 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) {
{isLoading && (
<div className="grid gap-4 grid-cols-1 lg:grid-cols-2">
{["skeleton-a", "skeleton-b", "skeleton-c"].map((key) => (
<Card key={key} className="border-border/60">
<CardContent className="p-4 md:p-5 space-y-4">
{/* Header: icon + title + status */}
<div className="flex items-start justify-between gap-3">
<div className="flex items-center gap-3 min-w-0">
<Skeleton className="h-9 w-9 rounded-lg shrink-0" />
<div className="space-y-1.5 flex-1">
<Skeleton className="h-4 w-24 md:w-28" />
<Skeleton className="h-3 w-40 md:w-52" />
</div>
</div>
<Skeleton className="h-4 w-4 rounded-full shrink-0" />
</div>
{/* Label */}
<div className="space-y-1.5">
<Skeleton className="h-3 w-20" />
<Skeleton className="h-9 md:h-10 w-full rounded-md" />
</div>
{/* Summary block */}
<div className="rounded-lg border border-border/50 p-3 space-y-2">
<div className="flex items-center gap-2">
<Skeleton className="h-3.5 w-3.5 rounded shrink-0" />
<Skeleton className="h-3.5 w-28" />
</div>
<div className="flex items-center gap-1.5">
<Skeleton className="h-4 w-14 rounded-full" />
<Skeleton className="h-3 w-24" />
</div>
</div>
<Card key={key} className="border-accent bg-accent/20">
<CardContent className="p-4 flex flex-col gap-3 min-h-32">
<Skeleton className="h-4 w-32 md:w-40 bg-accent" />
<Skeleton className="h-3 w-full bg-accent" />
<Skeleton className="h-3 w-24 md:w-28 bg-accent mt-auto" />
</CardContent>
</Card>
))}
@ -314,7 +290,7 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) {
return (
<div key={key}>
<Card className="group relative overflow-hidden transition-all duration-200 border-border/60 hover:shadow-md h-full">
<Card className="group relative overflow-hidden transition-all duration-200 border-accent bg-accent/20 hover:shadow-md h-full">
<CardContent className="p-4 md:p-5 space-y-4">
{/* Role Header */}
<div className="flex items-start justify-between gap-3">

View file

@ -6,7 +6,6 @@ import { useEffect, useState } from "react";
import { toast } from "sonner";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { Skeleton } from "@/components/ui/skeleton";
import { Textarea } from "@/components/ui/textarea";
@ -87,16 +86,16 @@ export function PromptConfigManager({ searchSpaceId }: PromptConfigManagerProps)
if (loading) {
return (
<div className="space-y-4 md:space-y-6">
<Card>
<CardHeader className="px-3 md:px-6 pt-3 md:pt-6 pb-2 md:pb-3">
<div className="space-y-3 md:space-y-4">
<div className="space-y-2">
<Skeleton className="h-5 md:h-6 w-36 md:w-48" />
<Skeleton className="h-3 md:h-4 w-full max-w-md mt-2" />
</CardHeader>
<CardContent className="space-y-3 md:space-y-4 px-3 md:px-6 pb-3 md:pb-6">
</div>
<div className="space-y-3 md:space-y-4">
<Skeleton className="h-16 md:h-20 w-full" />
<Skeleton className="h-24 md:h-32 w-full" />
</CardContent>
</Card>
</div>
</div>
</div>
);
}
@ -124,15 +123,17 @@ export function PromptConfigManager({ searchSpaceId }: PromptConfigManagerProps)
{/* System Instructions Card */}
<form onSubmit={onSubmit} className="space-y-4 md:space-y-6">
<Card>
<CardHeader className="px-3 md:px-6 pt-3 md:pt-6 pb-2 md:pb-3">
<CardTitle className="text-base md:text-lg">Custom System Instructions</CardTitle>
<CardDescription className="text-xs md:text-sm">
<div className="space-y-3 md:space-y-4">
<div className="space-y-1.5 md:space-y-2">
<h3 className="text-base md:text-lg font-semibold tracking-tight">
Custom System Instructions
</h3>
<p className="text-xs md:text-sm text-muted-foreground">
Provide specific guidelines for how you want the AI to respond. These instructions
will be applied to all answers in this search space.
</CardDescription>
</CardHeader>
<CardContent className="space-y-3 md:space-y-4 px-3 md:px-6 pb-3 md:pb-6">
</p>
</div>
<div className="space-y-3 md:space-y-4">
<div className="space-y-1.5 md:space-y-2">
<Label
htmlFor="custom-instructions-settings"
@ -174,8 +175,8 @@ export function PromptConfigManager({ searchSpaceId }: PromptConfigManagerProps)
</AlertDescription>
</Alert>
)}
</CardContent>
</Card>
</div>
</div>
{/* Action Buttons */}
<div className="flex justify-end pt-3 md:pt-4">

View file

@ -349,14 +349,14 @@ export function RolesManager({ searchSpaceId }: { searchSpaceId: number }) {
function PermissionsBadge({ permissions }: { permissions: string[] }) {
if (permissions.includes("*")) {
return (
<div className="px-2.5 py-1 rounded-md bg-muted/50 border border-border/60 text-muted-foreground">
<span className="text-xs font-medium whitespace-nowrap">Full access</span>
<div className="rounded-md border-0 bg-muted px-1.5 py-0.5 text-muted-foreground">
<span className="text-[10px] font-medium whitespace-nowrap">Full access</span>
</div>
);
}
return (
<div className="px-2.5 py-1 rounded-md border border-border/60 bg-muted/50 text-muted-foreground">
<span className="text-xs font-medium whitespace-nowrap">
<div className="rounded-md border-0 bg-muted px-1.5 py-0.5 text-muted-foreground">
<span className="text-[10px] font-medium whitespace-nowrap">
{permissions.length} permissions
</span>
</div>

View file

@ -25,6 +25,7 @@ import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { Separator } from "@/components/ui/separator";
import { Skeleton } from "@/components/ui/skeleton";
import { Spinner } from "@/components/ui/spinner";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
@ -137,7 +138,7 @@ export function VisionModelManager({ searchSpaceId }: VisionModelManagerProps) {
<Button
variant="outline"
onClick={openNewDialog}
className="gap-2 bg-white text-black hover:bg-accent hover:text-accent-foreground dark:bg-white dark:text-black"
className="gap-2 border-transparent bg-white text-[#1f1f1f] font-medium hover:bg-zinc-100 hover:text-[#1f1f1f] dark:border-transparent dark:bg-white dark:text-[#1f1f1f]"
>
Add Vision Model
</Button>
@ -184,37 +185,45 @@ export function VisionModelManager({ searchSpaceId }: VisionModelManagerProps) {
</div>
)}
{globalConfigs.filter((g) => !("is_auto_mode" in g && g.is_auto_mode)).length > 0 && (
{(isLoading ||
globalConfigs.filter((g) => !("is_auto_mode" in g && g.is_auto_mode)).length > 0) && (
<Alert>
<Info />
<AlertDescription>
<p>
<span className="font-medium">
{globalConfigs.filter((g) => !("is_auto_mode" in g && g.is_auto_mode)).length}{" "}
global vision{" "}
{globalConfigs.filter((g) => !("is_auto_mode" in g && g.is_auto_mode)).length === 1
? "model"
: "models"}
</span>{" "}
available from your administrator. {(() => {
const nonAuto = globalConfigs.filter(
(g) => !("is_auto_mode" in g && g.is_auto_mode)
);
const premium = nonAuto.filter(
(g) =>
"billing_tier" in g &&
(g as { billing_tier?: string }).billing_tier === "premium"
).length;
const free = nonAuto.length - premium;
if (premium > 0 && free > 0) {
return `${premium} premium, ${free} free.`;
}
if (premium > 0) {
return `All ${premium} premium — debits your shared credit pool.`;
}
return `All ${free} free.`;
})()}
</p>
{isLoading ? (
<div className="flex min-h-[1.625em] items-center">
<Skeleton className="h-4 w-60 bg-accent-foreground/15" />
</div>
) : (
<p>
<span className="font-medium">
{globalConfigs.filter((g) => !("is_auto_mode" in g && g.is_auto_mode)).length}{" "}
global vision{" "}
{globalConfigs.filter((g) => !("is_auto_mode" in g && g.is_auto_mode)).length ===
1
? "model"
: "models"}
</span>{" "}
available from your administrator. {(() => {
const nonAuto = globalConfigs.filter(
(g) => !("is_auto_mode" in g && g.is_auto_mode)
);
const premium = nonAuto.filter(
(g) =>
"billing_tier" in g &&
(g as { billing_tier?: string }).billing_tier === "premium"
).length;
const free = nonAuto.length - premium;
if (premium > 0 && free > 0) {
return `${premium} premium, ${free} free.`;
}
if (premium > 0) {
return `All ${premium} premium — debits your shared credit pool.`;
}
return `All ${free} free.`;
})()}
</p>
)}
</AlertDescription>
</Alert>
)}
@ -226,9 +235,6 @@ export function VisionModelManager({ searchSpaceId }: VisionModelManagerProps) {
{!isLoading &&
globalConfigs.filter((g) => !("is_auto_mode" in g && g.is_auto_mode)).length > 0 && (
<div className="space-y-3">
<h3 className="text-xs md:text-sm font-semibold text-muted-foreground">
Global Vision Models
</h3>
<div className="grid gap-3 grid-cols-1 sm:grid-cols-2 xl:grid-cols-3">
{globalConfigs
.filter((g) => !("is_auto_mode" in g && g.is_auto_mode))
@ -242,7 +248,7 @@ export function VisionModelManager({ searchSpaceId }: VisionModelManagerProps) {
return (
<Card
key={cfg.id}
className="border-border/60 bg-muted/20 overflow-hidden h-full"
className="group relative overflow-hidden transition-all duration-200 border-accent bg-accent/20 hover:shadow-md h-full"
>
<CardContent className="p-4 flex flex-col gap-3 h-full">
<div className="flex items-center gap-2 min-w-0">
@ -275,10 +281,13 @@ export function VisionModelManager({ searchSpaceId }: VisionModelManagerProps) {
{cfg.description}
</p>
)}
<div className="flex items-center pt-2 border-t border-border/40 mt-auto">
<span className="text-[11px] text-muted-foreground/60 truncate">
{cfg.model_name}
</span>
<div className="mt-auto space-y-2">
<Separator className="bg-accent" />
<div className="flex items-center">
<span className="text-[11px] text-muted-foreground/60 truncate">
{cfg.model_name}
</span>
</div>
</div>
</CardContent>
</Card>
@ -291,29 +300,13 @@ export function VisionModelManager({ searchSpaceId }: VisionModelManagerProps) {
{isLoading && (
<div className="space-y-4 md:space-y-6">
<div className="space-y-4">
<div className="flex items-center justify-between">
<Skeleton className="h-6 md:h-7 w-40 md:w-48" />
<Skeleton className="h-8 md:h-9 w-32 md:w-36 rounded-md" />
</div>
<div className="grid gap-3 grid-cols-1 sm:grid-cols-2 xl:grid-cols-3">
{["skeleton-a", "skeleton-b", "skeleton-c"].map((key) => (
<Card key={key} className="border-border/60">
<CardContent className="p-4 flex flex-col gap-3">
<div className="flex items-center gap-2.5">
<Skeleton className="size-4 rounded-full shrink-0" />
<div className="space-y-1.5 flex-1 min-w-0">
<Skeleton className="h-4 w-28 md:w-32" />
<Skeleton className="h-3 w-40 md:w-48" />
</div>
</div>
<div className="flex items-center pt-2 border-t border-border/40">
<Skeleton className="h-3 w-20 flex-1" />
<Skeleton className="h-3 w-3 rounded-full shrink-0 mx-1" />
<div className="flex-1 flex items-center justify-end gap-1.5">
<Skeleton className="h-4 w-4 rounded-full" />
<Skeleton className="h-3 w-16" />
</div>
</div>
<Card key={key} className="border-accent bg-accent/20">
<CardContent className="p-4 flex flex-col gap-3 min-h-32">
<Skeleton className="h-4 w-32 md:w-40 bg-accent" />
<Skeleton className="h-3 w-full bg-accent" />
<Skeleton className="h-3 w-24 md:w-28 bg-accent mt-auto" />
</CardContent>
</Card>
))}
@ -342,7 +335,7 @@ export function VisionModelManager({ searchSpaceId }: VisionModelManagerProps) {
return (
<div key={config.id}>
<Card className="group relative overflow-hidden transition-all duration-200 border-border/60 hover:shadow-md h-full">
<Card className="group relative overflow-hidden transition-all duration-200 border-accent bg-accent/20 hover:shadow-md h-full">
<CardContent className="p-4 flex flex-col gap-3 h-full">
{/* Header: Icon + Name + Actions */}
<div className="flex items-center justify-between gap-2">
@ -402,41 +395,44 @@ export function VisionModelManager({ searchSpaceId }: VisionModelManagerProps) {
</div>
{/* Footer: Date + Creator */}
<div className="flex items-center pt-2 border-t border-border/40 mt-auto">
<span className="shrink-0 text-[11px] text-muted-foreground/60 whitespace-nowrap">
{new Date(config.created_at).toLocaleDateString(undefined, {
year: "numeric",
month: "short",
day: "numeric",
})}
</span>
{member && (
<>
<Dot className="h-4 w-4 text-muted-foreground/30 shrink-0" />
<TooltipProvider>
<Tooltip open={isDesktop ? undefined : false}>
<TooltipTrigger asChild>
<div className="min-w-0 flex items-center gap-1.5 cursor-default">
<Avatar className="size-4.5 shrink-0">
{member.avatarUrl && (
<AvatarImage src={member.avatarUrl} alt={member.name} />
)}
<AvatarFallback className="text-[9px]">
{getInitials(member.name)}
</AvatarFallback>
</Avatar>
<span className="text-[11px] text-muted-foreground/60 truncate">
{member.name}
</span>
</div>
</TooltipTrigger>
<TooltipContent side="bottom">
{member.email || member.name}
</TooltipContent>
</Tooltip>
</TooltipProvider>
</>
)}
<div className="mt-auto space-y-2">
<Separator className="bg-accent" />
<div className="flex items-center">
<span className="shrink-0 text-[11px] text-muted-foreground/60 whitespace-nowrap">
{new Date(config.created_at).toLocaleDateString(undefined, {
year: "numeric",
month: "short",
day: "numeric",
})}
</span>
{member && (
<>
<Dot className="h-4 w-4 text-muted-foreground/30 shrink-0" />
<TooltipProvider>
<Tooltip open={isDesktop ? undefined : false}>
<TooltipTrigger asChild>
<div className="min-w-0 flex items-center gap-1.5 cursor-default">
<Avatar className="size-4.5 shrink-0">
{member.avatarUrl && (
<AvatarImage src={member.avatarUrl} alt={member.name} />
)}
<AvatarFallback className="text-[9px]">
{getInitials(member.name)}
</AvatarFallback>
</Avatar>
<span className="text-[11px] text-muted-foreground/60 truncate">
{member.name}
</span>
</div>
</TooltipTrigger>
<TooltipContent side="bottom">
{member.email || member.name}
</TooltipContent>
</Tooltip>
</TooltipProvider>
</>
)}
</div>
</div>
</CardContent>
</Card>