mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-23 19:05:16 +02:00
refactor: enhance user settings components with updated icons, improved loading states, and consistent alert structures
This commit is contained in:
parent
c8f0f7cb1b
commit
49e1395299
16 changed files with 703 additions and 649 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue