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

@ -2,7 +2,7 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
import { AlertTriangle, Check, Plus, ShieldCheck, Trash2, X } from "lucide-react"; import { AlertTriangle, Info, ShieldCheck, Trash2 } from "lucide-react";
import { useCallback, useMemo, useState } from "react"; import { useCallback, useMemo, useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { agentFlagsAtom } from "@/atoms/agent/agent-flags-query.atom"; import { agentFlagsAtom } from "@/atoms/agent/agent-flags-query.atom";
@ -20,8 +20,18 @@ import {
} from "@/components/ui/alert-dialog"; } from "@/components/ui/alert-dialog";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Skeleton } from "@/components/ui/skeleton";
import { import {
Select, Select,
SelectContent, SelectContent,
@ -67,20 +77,29 @@ function permissionRulesQueryKey(searchSpaceId: number) {
function ScopeBadge({ rule }: { rule: AgentPermissionRule }) { function ScopeBadge({ rule }: { rule: AgentPermissionRule }) {
if (rule.thread_id !== null) { if (rule.thread_id !== null) {
return ( return (
<Badge variant="outline" className="text-[10px]"> <Badge
variant="secondary"
className="text-[10px] px-1.5 py-0.5 border-0 text-muted-foreground bg-muted"
>
Thread #{rule.thread_id} Thread #{rule.thread_id}
</Badge> </Badge>
); );
} }
if (rule.user_id !== null) { if (rule.user_id !== null) {
return ( return (
<Badge variant="outline" className="text-[10px]"> <Badge
variant="secondary"
className="text-[10px] px-1.5 py-0.5 border-0 text-muted-foreground bg-muted"
>
User-specific User-specific
</Badge> </Badge>
); );
} }
return ( return (
<Badge variant="outline" className="text-[10px]"> <Badge
variant="secondary"
className="text-[10px] px-1.5 py-0.5 border-0 text-muted-foreground bg-muted"
>
Search space Search space
</Badge> </Badge>
); );
@ -170,8 +189,8 @@ export function AgentPermissionsContent() {
permission: formData.permission.trim(), permission: formData.permission.trim(),
pattern: formData.pattern.trim() || "*", pattern: formData.pattern.trim() || "*",
}); });
setShowForm(false);
setFormData(EMPTY_FORM); setFormData(EMPTY_FORM);
setShowForm(false);
} catch (err) { } catch (err) {
if (err instanceof AppError && err.message) { if (err instanceof AppError && err.message) {
// already toasted by onError // already toasted by onError
@ -190,13 +209,15 @@ export function AgentPermissionsContent() {
if (!featureEnabled) { if (!featureEnabled) {
return ( return (
<Alert className="border-dashed"> <Alert>
<ShieldCheck className="size-4" /> <Info />
<AlertTitle>Permission middleware is disabled</AlertTitle> <AlertTitle>Permission middleware is disabled</AlertTitle>
<AlertDescription> <AlertDescription>
Flip{" "} <p>
<code className="rounded bg-muted px-1 text-[10px]">SURFSENSE_ENABLE_PERMISSION</code> on Flip{" "}
the backend to manage allow/deny/ask rules from this panel. <code className="rounded bg-popover px-1 py-0.5 text-[10px] text-popover-foreground">SURFSENSE_ENABLE_PERMISSION</code> on
the backend to manage allow/deny/ask rules from this panel.
</p>
</AlertDescription> </AlertDescription>
</Alert> </Alert>
); );
@ -208,28 +229,8 @@ export function AgentPermissionsContent() {
); );
} }
if (isLoading) {
return (
<div className="flex items-center justify-center py-12">
<Spinner className="size-6" />
</div>
);
}
if (isError) {
return (
<Alert variant="destructive">
<AlertTriangle />
<AlertTitle>Failed to load rules</AlertTitle>
<AlertDescription>
{error instanceof Error ? error.message : "Unknown error."}
</AlertDescription>
</Alert>
);
}
return ( return (
<div className="min-w-0 space-y-6 overflow-hidden"> <div className="min-w-0 space-y-6 overflow-visible">
<div className="flex items-start justify-between gap-3"> <div className="flex items-start justify-between gap-3">
<div className="space-y-1"> <div className="space-y-1">
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
@ -237,27 +238,36 @@ export function AgentPermissionsContent() {
patterns and are evaluated at the most specific scope first. patterns and are evaluated at the most specific scope first.
</p> </p>
</div> </div>
{!showForm && ( <Button
<Button size="sm"
size="sm" onClick={() => {
onClick={() => { setShowForm(true);
setShowForm(true); setFormData(EMPTY_FORM);
setFormData(EMPTY_FORM); }}
}} className="shrink-0 gap-1.5"
className="shrink-0 gap-1.5" >
> New rule
<Plus className="size-3.5" /> </Button>
New rule
</Button>
)}
</div> </div>
{showForm && ( <Dialog
<div className="rounded-lg border border-border/60 bg-card p-6"> open={showForm}
<div className="space-y-4"> onOpenChange={(open) => {
<h3 className="text-sm font-semibold tracking-tight">New permission rule</h3> setShowForm(open);
if (!open) setFormData(EMPTY_FORM);
}}
>
<DialogContent className="max-w-lg bg-popover text-popover-foreground">
<DialogHeader>
<DialogTitle>New permission rule</DialogTitle>
<DialogDescription>
Tell the agent whether matching tool calls should be allowed, denied, or paused for
approval.
</DialogDescription>
</DialogHeader>
<div className="grid grid-cols-2 gap-3"> <div className="space-y-4">
<div className="grid gap-3">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="permission-name">Permission</Label> <Label htmlFor="permission-name">Permission</Label>
<Input <Input
@ -306,34 +316,60 @@ export function AgentPermissionsContent() {
{ACTION_DESCRIPTIONS[formData.action]} {ACTION_DESCRIPTIONS[formData.action]}
</p> </p>
</div> </div>
<div className="flex items-center justify-end gap-2 pt-2">
<Button
variant="ghost"
size="sm"
onClick={() => {
setShowForm(false);
setFormData(EMPTY_FORM);
}}
disabled={createMutation.isPending}
>
Cancel
</Button>
<Button
size="sm"
onClick={handleCreate}
disabled={createMutation.isPending || !formData.permission.trim()}
className="relative"
>
<span className={createMutation.isPending ? "opacity-0" : ""}>Create</span>
{createMutation.isPending && <Spinner className="absolute size-3.5" />}
</Button>
</div>
</div> </div>
<DialogFooter>
<Button
type="button"
variant="secondary"
size="sm"
onClick={() => {
setShowForm(false);
setFormData(EMPTY_FORM);
}}
disabled={createMutation.isPending}
className="text-sm h-9"
>
Cancel
</Button>
<Button
size="sm"
onClick={handleCreate}
disabled={createMutation.isPending || !formData.permission.trim()}
className="relative text-sm h-9 min-w-[96px]"
>
<span className={createMutation.isPending ? "opacity-0" : ""}>Create</span>
{createMutation.isPending && <Spinner size="sm" className="absolute" />}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{isLoading && (
<div className="-m-1 space-y-2 p-1">
{["skeleton-a", "skeleton-b", "skeleton-c"].map((key) => (
<Card key={key} className="border-accent bg-accent/20">
<CardContent className="p-4 flex flex-col gap-3 min-h-24">
<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>
))}
</div> </div>
)} )}
{sortedRules.length === 0 && !showForm && ( {isError && (
<Alert variant="destructive">
<AlertTriangle />
<AlertTitle>Failed to load rules</AlertTitle>
<AlertDescription>
{error instanceof Error ? error.message : "Unknown error."}
</AlertDescription>
</Alert>
)}
{!isLoading && !isError && sortedRules.length === 0 && !showForm && (
<div className="rounded-lg border border-dashed border-border/60 p-8 text-center"> <div className="rounded-lg border border-dashed border-border/60 p-8 text-center">
<ShieldCheck className="mx-auto size-8 text-muted-foreground/40" /> <ShieldCheck className="mx-auto size-8 text-muted-foreground/40" />
<p className="mt-2 text-sm text-muted-foreground">No rules yet</p> <p className="mt-2 text-sm text-muted-foreground">No rules yet</p>
@ -343,8 +379,8 @@ export function AgentPermissionsContent() {
</div> </div>
)} )}
{sortedRules.length > 0 && ( {!isLoading && !isError && sortedRules.length > 0 && (
<div className="space-y-2"> <div className="-m-1 space-y-2 p-1">
{sortedRules.map((rule) => { {sortedRules.map((rule) => {
const badge = ACTION_BADGE[rule.action]; const badge = ACTION_BADGE[rule.action];
const isUpdating = const isUpdating =
@ -352,14 +388,14 @@ export function AgentPermissionsContent() {
const isDeleting = deleteMutation.isPending && deleteMutation.variables === rule.id; const isDeleting = deleteMutation.isPending && deleteMutation.variables === rule.id;
return ( return (
<div <Card
key={rule.id} key={rule.id}
className="group flex flex-col gap-3 rounded-lg border border-border/60 bg-card p-4" className="group relative overflow-hidden transition-all duration-200 border-accent bg-accent/20 hover:shadow-md h-full"
> >
<div className="flex items-start justify-between gap-3"> <CardContent className="p-4 flex items-center justify-between gap-3 h-full">
<div className="flex min-w-0 flex-1 flex-col gap-1.5"> <div className="flex min-w-0 flex-1 flex-col gap-1.5">
<div className="flex flex-wrap items-center gap-1.5"> <div className="flex flex-wrap items-center gap-1.5">
<code className="truncate rounded bg-muted px-1.5 py-0.5 font-mono text-xs"> <code className="truncate font-mono text-sm font-medium text-foreground">
{rule.permission} {rule.permission}
</code> </code>
{rule.pattern !== "*" && ( {rule.pattern !== "*" && (
@ -374,7 +410,7 @@ export function AgentPermissionsContent() {
</p> </p>
</div> </div>
<div className="flex shrink-0 items-center gap-1"> <div className="flex shrink-0 items-center self-center gap-1">
<Select <Select
value={rule.action} value={rule.action}
onValueChange={(value) => onValueChange={(value) =>
@ -390,8 +426,6 @@ export function AgentPermissionsContent() {
> >
<SelectValue> <SelectValue>
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
{rule.action === "allow" && <Check className="size-3" />}
{rule.action === "deny" && <X className="size-3" />}
{badge.label} {badge.label}
</span> </span>
</SelectValue> </SelectValue>
@ -406,7 +440,7 @@ export function AgentPermissionsContent() {
<Button <Button
size="sm" size="sm"
variant="ghost" variant="ghost"
className="size-8 p-0 text-muted-foreground hover:text-destructive" className="h-7 w-7 rounded-lg p-0 text-muted-foreground hover:text-destructive"
onClick={() => setDeleteTarget(rule.id)} onClick={() => setDeleteTarget(rule.id)}
disabled={isUpdating || isDeleting} disabled={isUpdating || isDeleting}
aria-label="Delete rule" aria-label="Delete rule"
@ -414,8 +448,8 @@ export function AgentPermissionsContent() {
<Trash2 className="size-3.5" /> <Trash2 className="size-3.5" />
</Button> </Button>
</div> </div>
</div> </CardContent>
</div> </Card>
); );
})} })}
</div> </div>

View file

@ -1,12 +1,11 @@
"use client"; "use client";
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
import { CircleCheck, CircleSlash, Cog, RotateCcw } from "lucide-react"; import { AlertTriangle, CircleCheck, CircleSlash, Info } from "lucide-react";
import { useMemo } from "react"; import { Fragment, 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";
@ -223,7 +222,7 @@ function FlagRow({ def, value }: { def: FlagDef; value: boolean }) {
} }
export function AgentStatusContent() { export function AgentStatusContent() {
const { data: flags, isLoading, isError, error, refetch } = useAtomValue(agentFlagsAtom); const { data: flags, isLoading, isError, error } = useAtomValue(agentFlagsAtom);
const enabledCount = useMemo(() => { const enabledCount = useMemo(() => {
if (!flags) return 0; if (!flags) return 0;
@ -244,19 +243,10 @@ export function AgentStatusContent() {
if (isError || !flags) { if (isError || !flags) {
return ( return (
<Alert variant="destructive"> <Alert variant="destructive">
<AlertTriangle />
<AlertTitle>Failed to load agent status</AlertTitle> <AlertTitle>Failed to load agent status</AlertTitle>
<AlertDescription className="flex items-center gap-2"> <AlertDescription>
{error instanceof Error ? error.message : "Unknown error."} {error instanceof Error ? error.message : "Unknown error."}
<Button
type="button"
variant="outline"
size="sm"
onClick={() => refetch()}
className="ml-auto h-auto gap-1 px-2 py-0.5 text-xs hover:bg-background"
>
<RotateCcw className="size-3" />
Retry
</Button>
</AlertDescription> </AlertDescription>
</Alert> </Alert>
); );
@ -268,28 +258,36 @@ export function AgentStatusContent() {
<div className="space-y-6"> <div className="space-y-6">
{masterOff ? ( {masterOff ? (
<Alert variant="destructive"> <Alert variant="destructive">
<Cog className="size-4" /> <AlertTriangle />
<AlertTitle>Master kill-switch is on</AlertTitle> <AlertTitle>Master kill-switch is on</AlertTitle>
<AlertDescription> <AlertDescription>
<code className="rounded bg-muted px-1 text-[10px]"> <p>
SURFSENSE_DISABLE_NEW_AGENT_STACK=true Showing that{" "}
</code> <code className="rounded bg-muted px-1 text-[10px]">
forces every new middleware off, regardless of the individual flags below. Restart the SURFSENSE_DISABLE_NEW_AGENT_STACK=true
backend after changing it. </code>
, which forces every new middleware off, regardless of the individual flags below.
Restart the backend after changing it.
</p>
</AlertDescription> </AlertDescription>
</Alert> </Alert>
) : ( ) : (
<Alert> <Alert>
<Cog className="size-4" /> <Info />
<AlertTitle className="flex items-center gap-2"> <AlertTitle className="flex items-center gap-2">
Agent stack Agent stack
<Badge variant="secondary" className="text-[10px]"> <Badge
variant="secondary"
className="rounded bg-popover px-1 py-0.5 text-[9px] text-popover-foreground"
>
{enabledCount} on {enabledCount} on
</Badge> </Badge>
</AlertTitle> </AlertTitle>
<AlertDescription> <AlertDescription>
Read-only mirror of the backend's <code>AgentFeatureFlags</code>. Flip an env var and <p>
restart the backend to change a value. Showing a read-only mirror of the backend's <code>AgentFeatureFlags</code>. Flip an env
var and restart the backend to change a value.
</p>
</AlertDescription> </AlertDescription>
</Alert> </Alert>
)} )}
@ -298,9 +296,9 @@ export function AgentStatusContent() {
const allOff = group.flags.every((f) => !flags[f.key]); const allOff = group.flags.every((f) => !flags[f.key]);
return ( return (
<div key={group.id}> <div key={group.id}>
{groupIdx > 0 && <Separator className="my-4" />} {groupIdx > 0 && <Separator className="my-4 bg-border" />}
<div className="rounded-lg border border-border/60 bg-card"> <div className="rounded-lg border border-border/60 bg-card">
<div className="flex items-start justify-between gap-3 border-b px-4 py-3"> <div className="flex items-start justify-between gap-3 px-4 py-3">
<div> <div>
<p className="text-sm font-semibold">{group.title}</p> <p className="text-sm font-semibold">{group.title}</p>
<p className="text-xs text-muted-foreground">{group.subtitle}</p> <p className="text-xs text-muted-foreground">{group.subtitle}</p>
@ -311,9 +309,13 @@ export function AgentStatusContent() {
</Badge> </Badge>
)} )}
</div> </div>
<div className="divide-y divide-border/50 px-4"> <Separator className="bg-border" />
{group.flags.map((def) => ( <div className="px-4">
<FlagRow key={def.key} def={def} value={flags[def.key]} /> {group.flags.map((def, flagIdx) => (
<Fragment key={def.key}>
{flagIdx > 0 && <Separator className="bg-border" />}
<FlagRow def={def} value={flags[def.key]} />
</Fragment>
))} ))}
</div> </div>
</div> </div>

View file

@ -5,6 +5,7 @@ import { useTranslations } from "next-intl";
import { useCallback, useRef, useState } from "react"; import { useCallback, useRef, useState } from "react";
import { Alert, AlertDescription } from "@/components/ui/alert"; import { Alert, AlertDescription } from "@/components/ui/alert";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { useApiKey } from "@/hooks/use-api-key"; import { useApiKey } from "@/hooks/use-api-key";
import { copyToClipboard as copyToClipboardUtil } from "@/lib/utils"; import { copyToClipboard as copyToClipboardUtil } from "@/lib/utils";
@ -35,7 +36,12 @@ export function ApiKeyContent() {
<div className="min-w-0 overflow-hidden"> <div className="min-w-0 overflow-hidden">
<h3 className="mb-4 text-sm font-semibold tracking-tight">{t("your_api_key")}</h3> <h3 className="mb-4 text-sm font-semibold tracking-tight">{t("your_api_key")}</h3>
{isLoading ? ( {isLoading ? (
<div className="h-12 w-full animate-pulse rounded-md border border-border/60 bg-muted/30" /> <div className="flex items-center gap-2 rounded-md border border-border/60 bg-muted/30 px-2.5 py-1.5">
<div className="min-w-0 flex-1 overflow-hidden">
<Skeleton className="h-3 w-full bg-accent" />
</div>
<div className="h-6 w-6 shrink-0" />
</div>
) : apiKey ? ( ) : apiKey ? (
<div className="flex items-center gap-2 rounded-md border border-border/60 bg-muted/30 px-2.5 py-1.5"> <div className="flex items-center gap-2 rounded-md border border-border/60 bg-muted/30 px-2.5 py-1.5">
<div className="min-w-0 flex-1 overflow-x-auto scrollbar-hide"> <div className="min-w-0 flex-1 overflow-x-auto scrollbar-hide">

View file

@ -1,12 +1,14 @@
"use client"; "use client";
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
import { AlertTriangle, Copy, Globe, Sparkles } from "lucide-react"; import { AlertTriangle, Copy, Library } from "lucide-react";
import { useCallback, useState } from "react"; import { useCallback, useState } from "react";
import { copyPromptMutationAtom } from "@/atoms/prompts/prompts-mutation.atoms"; import { copyPromptMutationAtom } from "@/atoms/prompts/prompts-mutation.atoms";
import { publicPromptsAtom } from "@/atoms/prompts/prompts-query.atoms"; import { publicPromptsAtom } from "@/atoms/prompts/prompts-query.atoms";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";
import { Spinner } from "@/components/ui/spinner"; import { Spinner } from "@/components/ui/spinner";
export function CommunityPromptsContent() { export function CommunityPromptsContent() {
@ -35,33 +37,37 @@ export function CommunityPromptsContent() {
const list = prompts ?? []; const list = prompts ?? [];
if (isLoading) {
return (
<div className="flex items-center justify-center py-12">
<Spinner className="size-6" />
</div>
);
}
if (isError) {
return (
<Alert variant="destructive">
<AlertTriangle />
<AlertTitle>Failed to load community prompts</AlertTitle>
<AlertDescription>Please try refreshing the page.</AlertDescription>
</Alert>
);
}
return ( return (
<div className="space-y-6 min-w-0 overflow-hidden"> <div className="space-y-6 min-w-0 overflow-hidden">
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
Prompts shared by other users. Add any to your collection with one click. Prompts shared by other users. Add any to your collection with one click.
</p> </p>
{list.length === 0 && ( {isLoading && (
<div className="space-y-2">
{["skeleton-a", "skeleton-b", "skeleton-c"].map((key) => (
<Card key={key} className="border-accent bg-accent/20">
<CardContent className="p-4 flex flex-col gap-3 min-h-24">
<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>
))}
</div>
)}
{isError && (
<Alert variant="destructive">
<AlertTriangle />
<AlertTitle>Failed to load community prompts</AlertTitle>
<AlertDescription>Please try refreshing the page.</AlertDescription>
</Alert>
)}
{!isLoading && !isError && list.length === 0 && (
<div className="rounded-lg border border-dashed border-border/60 p-8 text-center"> <div className="rounded-lg border border-dashed border-border/60 p-8 text-center">
<Globe className="mx-auto size-8 text-muted-foreground" /> <Library className="mx-auto size-8 text-muted-foreground" />
<p className="mt-2 text-sm text-muted-foreground">No community prompts yet</p> <p className="mt-2 text-sm text-muted-foreground">No community prompts yet</p>
<p className="text-xs text-muted-foreground/60"> <p className="text-xs text-muted-foreground/60">
Share your own prompts from the My Prompts tab Share your own prompts from the My Prompts tab
@ -69,59 +75,58 @@ export function CommunityPromptsContent() {
</div> </div>
)} )}
{list.length > 0 && ( {!isLoading && !isError && list.length > 0 && (
<div className="space-y-2"> <div className="space-y-2">
{list.map((prompt) => ( {list.map((prompt) => (
<div <Card
key={prompt.id} key={prompt.id}
className="group flex items-start gap-3 rounded-lg border border-border/60 bg-card p-4" className="group relative overflow-hidden transition-all duration-200 border-accent bg-accent/20 hover:shadow-md h-full"
> >
<div className="mt-0.5 shrink-0 text-muted-foreground"> <CardContent className="p-4 flex items-start gap-3 h-full">
<Sparkles className="size-4" /> <div className="flex-1 min-w-0">
</div> <div className="flex items-center gap-2">
<div className="flex-1 min-w-0"> <span className="text-sm font-medium">{prompt.name}</span>
<div className="flex items-center gap-2"> <span className="rounded-md border-0 bg-muted px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground">
<span className="text-sm font-medium">{prompt.name}</span> {prompt.mode}
<span className="rounded-full border px-2 py-0.5 text-[10px] text-muted-foreground">
{prompt.mode}
</span>
{prompt.author_name && (
<span className="text-[11px] text-muted-foreground/60">
by {prompt.author_name}
</span> </span>
{prompt.author_name && (
<span className="text-[11px] text-muted-foreground/60">
by {prompt.author_name}
</span>
)}
</div>
<p
className={`mt-1 text-xs text-muted-foreground ${expandedId === prompt.id ? "whitespace-pre-wrap" : "line-clamp-2"}`}
>
{prompt.prompt}
</p>
{prompt.prompt.length > 100 && (
<Button
type="button"
variant="link"
onClick={() => setExpandedId(expandedId === prompt.id ? null : prompt.id)}
className="mt-1 h-auto cursor-pointer px-0 py-0 text-[11px] text-primary"
>
{expandedId === prompt.id ? "See less" : "See more"}
</Button>
)} )}
</div> </div>
<p <Button
className={`mt-1 text-xs text-muted-foreground ${expandedId === prompt.id ? "whitespace-pre-wrap" : "line-clamp-2"}`} variant="ghost"
size="sm"
className="h-7 shrink-0 gap-1.5 rounded-lg px-2 text-muted-foreground hover:text-accent-foreground"
disabled={copyingIds.has(prompt.id)}
onClick={() => handleCopy(prompt.id)}
> >
{prompt.prompt} {copyingIds.has(prompt.id) ? (
</p> <Spinner className="size-3" />
{prompt.prompt.length > 100 && ( ) : (
<Button <Copy className="size-3" />
type="button" )}
variant="link" Add to mine
onClick={() => setExpandedId(expandedId === prompt.id ? null : prompt.id)} </Button>
className="mt-1 h-auto cursor-pointer px-0 py-0 text-[11px] text-primary" </CardContent>
> </Card>
{expandedId === prompt.id ? "See less" : "See more"}
</Button>
)}
</div>
<Button
variant="outline"
size="sm"
className="shrink-0 gap-1.5"
disabled={copyingIds.has(prompt.id)}
onClick={() => handleCopy(prompt.id)}
>
{copyingIds.has(prompt.id) ? (
<Spinner className="size-3" />
) : (
<Copy className="size-3" />
)}
Add to mine
</Button>
</div>
))} ))}
</div> </div>
)} )}

View file

@ -22,9 +22,26 @@ import {
} from "@/components/ui/alert-dialog"; } from "@/components/ui/alert-dialog";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { ShortcutKbd } from "@/components/ui/shortcut-kbd"; import { ShortcutKbd } from "@/components/ui/shortcut-kbd";
import { Skeleton } from "@/components/ui/skeleton";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Spinner } from "@/components/ui/spinner"; import { Spinner } from "@/components/ui/spinner";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import type { PromptRead } from "@/contracts/types/prompts.types"; import type { PromptRead } from "@/contracts/types/prompts.types";
@ -124,24 +141,6 @@ export function PromptsContent() {
const list = prompts ?? []; const list = prompts ?? [];
if (isLoading) {
return (
<div className="flex items-center justify-center py-12">
<Spinner className="size-6" />
</div>
);
}
if (isError) {
return (
<Alert variant="destructive">
<AlertTriangle />
<AlertTitle>Failed to load prompts</AlertTitle>
<AlertDescription>Please try refreshing the page.</AlertDescription>
</Alert>
);
}
return ( return (
<div className="space-y-6 min-w-0 overflow-hidden"> <div className="space-y-6 min-w-0 overflow-hidden">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
@ -149,97 +148,146 @@ export function PromptsContent() {
Create prompt templates triggered with <ShortcutKbd keys={["/"]} className="ml-0" /> in Create prompt templates triggered with <ShortcutKbd keys={["/"]} className="ml-0" /> in
the chat composer. the chat composer.
</p> </p>
{!showForm && ( <Button
<Button size="sm"
size="sm" onClick={() => {
onClick={() => { setShowForm(true);
setShowForm(true); setEditingId(null);
setEditingId(null); setFormData(EMPTY_FORM);
setFormData(EMPTY_FORM); }}
}} className="shrink-0 gap-1.5"
className="shrink-0 gap-1.5" >
> New
New </Button>
</Button>
)}
</div> </div>
{showForm && ( <Dialog
<div className="rounded-lg border border-border/60 bg-card p-6 space-y-4"> open={showForm}
<h3 className="text-sm font-semibold tracking-tight"> onOpenChange={(open) => {
{editingId !== null ? "Edit prompt" : "New prompt"} setShowForm(open);
</h3> if (!open) {
setFormData(EMPTY_FORM);
setEditingId(null);
}
}}
>
<DialogContent className="max-w-lg bg-popover text-popover-foreground">
<DialogHeader>
<DialogTitle>{editingId !== null ? "Edit prompt" : "New prompt"}</DialogTitle>
<DialogDescription>
Create prompt templates triggered with / in the chat composer.
</DialogDescription>
</DialogHeader>
<div className="space-y-2"> <div className="space-y-4">
<Label htmlFor="prompt-name">Name</Label> <div className="space-y-2">
<Input <Label htmlFor="prompt-name">Name</Label>
id="prompt-name" <Input
value={formData.name} id="prompt-name"
onChange={(e) => setFormData((p) => ({ ...p, name: e.target.value }))} value={formData.name}
placeholder="e.g. Fix grammar" onChange={(e) => setFormData((p) => ({ ...p, name: e.target.value }))}
/> placeholder="e.g. Fix grammar"
/>
</div>
<div className="space-y-2">
<Label htmlFor="prompt-template">Prompt template</Label>
<textarea
id="prompt-template"
value={formData.prompt}
onChange={(e) => setFormData((p) => ({ ...p, prompt: e.target.value }))}
placeholder="e.g. Fix the grammar in the following text:\n\n{selection}"
rows={4}
className="w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm outline-none resize-none focus:ring-1 focus:ring-ring"
/>
<p className="text-xs text-muted-foreground">
Use{" "}
<code className="rounded bg-muted px-1 py-0.5 font-mono text-[11px]">
{"{selection}"}
</code>{" "}
to insert the input text. If omitted, the text is appended automatically.
</p>
</div>
<div className="space-y-2">
<Label htmlFor="prompt-mode">Mode</Label>
<Select
value={formData.mode}
onValueChange={(value) =>
setFormData((p) => ({ ...p, mode: value as "transform" | "explore" }))
}
>
<SelectTrigger id="prompt-mode" className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="transform">Transform rewrites or modifies your text</SelectItem>
<SelectItem value="explore">Explore answers a question about your text</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex items-center gap-2">
<Switch
id="prompt-public"
checked={formData.is_public}
onCheckedChange={(checked) => setFormData((p) => ({ ...p, is_public: checked }))}
/>
<Label htmlFor="prompt-public" className="text-sm font-normal">
Share with community
</Label>
</div>
</div> </div>
<div className="space-y-2"> <DialogFooter>
<Label htmlFor="prompt-template">Prompt template</Label> <Button
<textarea type="button"
id="prompt-template" variant="secondary"
value={formData.prompt} size="sm"
onChange={(e) => setFormData((p) => ({ ...p, prompt: e.target.value }))} onClick={handleCancel}
placeholder="e.g. Fix the grammar in the following text:\n\n{selection}" disabled={isSaving}
rows={4} className="text-sm h-9"
className="w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm outline-none resize-none focus:ring-1 focus:ring-ring"
/>
<p className="text-xs text-muted-foreground">
Use{" "}
<code className="rounded bg-muted px-1 py-0.5 font-mono text-[11px]">
{"{selection}"}
</code>{" "}
to insert the input text. If omitted, the text is appended automatically.
</p>
</div>
<div className="space-y-2">
<Label htmlFor="prompt-mode">Mode</Label>
<select
id="prompt-mode"
value={formData.mode}
onChange={(e) =>
setFormData((p) => ({ ...p, mode: e.target.value as "transform" | "explore" }))
}
className="w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm outline-none focus:ring-1 focus:ring-ring"
> >
<option value="transform">Transform rewrites or modifies your text</option>
<option value="explore">Explore answers a question about your text</option>
</select>
</div>
<div className="flex items-center gap-2">
<Switch
id="prompt-public"
checked={formData.is_public}
onCheckedChange={(checked) => setFormData((p) => ({ ...p, is_public: checked }))}
/>
<Label htmlFor="prompt-public" className="text-sm font-normal">
Share with community
</Label>
</div>
<div className="flex items-center justify-end gap-2 pt-2">
<Button variant="ghost" size="sm" onClick={handleCancel}>
Cancel Cancel
</Button> </Button>
<Button size="sm" onClick={handleSave} disabled={isSaving} className="relative"> <Button
size="sm"
onClick={handleSave}
disabled={isSaving}
className="relative text-sm h-9 min-w-[96px]"
>
<span className={isSaving ? "opacity-0" : ""}> <span className={isSaving ? "opacity-0" : ""}>
{editingId !== null ? "Update" : "Create"} {editingId !== null ? "Update" : "Create"}
</span> </span>
{isSaving && <Spinner className="size-3.5 absolute" />} {isSaving && <Spinner size="sm" className="absolute" />}
</Button> </Button>
</div> </DialogFooter>
</DialogContent>
</Dialog>
{isLoading && (
<div className="space-y-2">
{["skeleton-a", "skeleton-b", "skeleton-c"].map((key) => (
<Card key={key} className="border-accent bg-accent/20">
<CardContent className="p-4 flex flex-col gap-3 min-h-24">
<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>
))}
</div> </div>
)} )}
{list.length === 0 && !showForm && ( {isError && (
<Alert variant="destructive">
<AlertTriangle />
<AlertTitle>Failed to load prompts</AlertTitle>
<AlertDescription>Please try refreshing the page.</AlertDescription>
</Alert>
)}
{!isLoading && !isError && list.length === 0 && !showForm && (
<div className="rounded-lg border border-dashed border-border/60 p-8 text-center"> <div className="rounded-lg border border-dashed border-border/60 p-8 text-center">
<Sparkles className="mx-auto size-8 text-muted-foreground/40" /> <Sparkles className="mx-auto size-8 text-muted-foreground/40" />
<p className="mt-2 text-sm text-muted-foreground">No prompts yet</p> <p className="mt-2 text-sm text-muted-foreground">No prompts yet</p>
@ -249,24 +297,21 @@ export function PromptsContent() {
</div> </div>
)} )}
{list.length > 0 && ( {!isLoading && !isError && list.length > 0 && (
<div className="space-y-2"> <div className="space-y-2">
{list.map((prompt) => ( {list.map((prompt) => (
<div <div
key={prompt.id} key={prompt.id}
className="group flex items-start gap-3 rounded-lg border border-border/60 bg-card p-4" className="group relative flex items-start gap-3 overflow-hidden rounded-lg border border-accent bg-accent/20 p-4 transition-all duration-200 hover:shadow-md"
> >
<div className="mt-0.5 shrink-0 text-muted-foreground">
<Sparkles className="size-4" />
</div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-sm font-medium">{prompt.name}</span> <span className="text-sm font-medium">{prompt.name}</span>
<span className="rounded-full border px-2 py-0.5 text-[10px] text-muted-foreground"> <span className="rounded-md border-0 bg-muted px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground">
{prompt.mode} {prompt.mode}
</span> </span>
{prompt.is_public && ( {prompt.is_public && (
<span className="flex items-center gap-1 rounded-full border border-primary/20 bg-primary/5 px-2 py-0.5 text-[10px] text-primary"> <span className="flex items-center gap-1 rounded-md border-0 bg-muted px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground">
<Globe className="size-2.5" /> <Globe className="size-2.5" />
Public Public
</span> </span>
@ -288,7 +333,7 @@ export function PromptsContent() {
</Button> </Button>
)} )}
</div> </div>
<div className="hidden group-hover:flex items-center gap-1 shrink-0"> <div className="flex items-center gap-1 shrink-0 opacity-0 pointer-events-none transition-opacity duration-150 group-hover:opacity-100 group-hover:pointer-events-auto">
<Button <Button
type="button" type="button"
variant="ghost" variant="ghost"
@ -296,7 +341,7 @@ export function PromptsContent() {
title={prompt.is_public ? "Make private" : "Share with community"} title={prompt.is_public ? "Make private" : "Share with community"}
onClick={() => handleTogglePublic(prompt)} onClick={() => handleTogglePublic(prompt)}
disabled={togglingPublicIds.has(prompt.id)} disabled={togglingPublicIds.has(prompt.id)}
className="size-7 text-muted-foreground hover:bg-accent hover:text-accent-foreground" className="h-7 w-7 rounded-lg text-muted-foreground hover:text-accent-foreground"
> >
{togglingPublicIds.has(prompt.id) ? ( {togglingPublicIds.has(prompt.id) ? (
<Spinner className="size-3.5" /> <Spinner className="size-3.5" />
@ -309,7 +354,7 @@ export function PromptsContent() {
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className="size-7" className="h-7 w-7 rounded-lg text-muted-foreground hover:text-accent-foreground"
onClick={() => handleEdit(prompt)} onClick={() => handleEdit(prompt)}
> >
<Pencil className="size-3.5" /> <Pencil className="size-3.5" />
@ -317,7 +362,7 @@ export function PromptsContent() {
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className="size-7 text-destructive hover:text-destructive" className="h-7 w-7 rounded-lg text-muted-foreground hover:text-destructive"
onClick={() => setDeleteTarget(prompt.id)} onClick={() => setDeleteTarget(prompt.id)}
> >
<Trash2 className="size-3.5" /> <Trash2 className="size-3.5" />

View file

@ -3,13 +3,13 @@
import { import {
Brain, Brain,
CircleUser, CircleUser,
Globe,
Keyboard, Keyboard,
KeyRound, KeyRound,
Library,
Monitor, Monitor,
ReceiptText, ReceiptText,
ShieldCheck, ShieldCheck,
Sparkles, WandSparkles,
Workflow, Workflow,
} from "lucide-react"; } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
@ -71,12 +71,12 @@ export function UserSettingsLayoutShell({
{ {
value: "prompts" as const, value: "prompts" as const,
label: "My Prompts", label: "My Prompts",
icon: <Sparkles className="h-4 w-4" />, icon: <WandSparkles className="h-4 w-4" />,
}, },
{ {
value: "community-prompts" as const, value: "community-prompts" as const,
label: "Community Prompts", label: "Community Prompts",
icon: <Globe className="h-4 w-4" />, icon: <Library className="h-4 w-4" />,
}, },
{ {
value: "memory" as const, value: "memory" as const,

View file

@ -22,7 +22,7 @@ export function AnnouncementsDialog() {
return ( return (
<Dialog open={open} onOpenChange={setOpen}> <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> <DialogTitle className="sr-only">What's New</DialogTitle>
<div className="flex flex-1 flex-col overflow-hidden min-w-0"> <div className="flex flex-1 flex-col overflow-hidden min-w-0">

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -236,35 +236,11 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) {
{isLoading && ( {isLoading && (
<div className="grid gap-4 grid-cols-1 lg:grid-cols-2"> <div className="grid gap-4 grid-cols-1 lg:grid-cols-2">
{["skeleton-a", "skeleton-b", "skeleton-c"].map((key) => ( {["skeleton-a", "skeleton-b", "skeleton-c"].map((key) => (
<Card key={key} className="border-border/60"> <Card key={key} className="border-accent bg-accent/20">
<CardContent className="p-4 md:p-5 space-y-4"> <CardContent className="p-4 flex flex-col gap-3 min-h-32">
{/* Header: icon + title + status */} <Skeleton className="h-4 w-32 md:w-40 bg-accent" />
<div className="flex items-start justify-between gap-3"> <Skeleton className="h-3 w-full bg-accent" />
<div className="flex items-center gap-3 min-w-0"> <Skeleton className="h-3 w-24 md:w-28 bg-accent mt-auto" />
<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>
</CardContent> </CardContent>
</Card> </Card>
))} ))}
@ -314,7 +290,7 @@ export function LLMRoleManager({ searchSpaceId }: LLMRoleManagerProps) {
return ( return (
<div key={key}> <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"> <CardContent className="p-4 md:p-5 space-y-4">
{/* Role Header */} {/* Role Header */}
<div className="flex items-start justify-between gap-3"> <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 { toast } from "sonner";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
@ -87,16 +86,16 @@ export function PromptConfigManager({ searchSpaceId }: PromptConfigManagerProps)
if (loading) { if (loading) {
return ( return (
<div className="space-y-4 md:space-y-6"> <div className="space-y-4 md:space-y-6">
<Card> <div className="space-y-3 md:space-y-4">
<CardHeader className="px-3 md:px-6 pt-3 md:pt-6 pb-2 md:pb-3"> <div className="space-y-2">
<Skeleton className="h-5 md:h-6 w-36 md:w-48" /> <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" /> <Skeleton className="h-3 md:h-4 w-full max-w-md mt-2" />
</CardHeader> </div>
<CardContent className="space-y-3 md:space-y-4 px-3 md:px-6 pb-3 md:pb-6"> <div className="space-y-3 md:space-y-4">
<Skeleton className="h-16 md:h-20 w-full" /> <Skeleton className="h-16 md:h-20 w-full" />
<Skeleton className="h-24 md:h-32 w-full" /> <Skeleton className="h-24 md:h-32 w-full" />
</CardContent> </div>
</Card> </div>
</div> </div>
); );
} }
@ -124,15 +123,17 @@ export function PromptConfigManager({ searchSpaceId }: PromptConfigManagerProps)
{/* System Instructions Card */} {/* System Instructions Card */}
<form onSubmit={onSubmit} className="space-y-4 md:space-y-6"> <form onSubmit={onSubmit} className="space-y-4 md:space-y-6">
<Card> <div className="space-y-3 md:space-y-4">
<CardHeader className="px-3 md:px-6 pt-3 md:pt-6 pb-2 md:pb-3"> <div className="space-y-1.5 md:space-y-2">
<CardTitle className="text-base md:text-lg">Custom System Instructions</CardTitle> <h3 className="text-base md:text-lg font-semibold tracking-tight">
<CardDescription className="text-xs md:text-sm"> 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 Provide specific guidelines for how you want the AI to respond. These instructions
will be applied to all answers in this search space. will be applied to all answers in this search space.
</CardDescription> </p>
</CardHeader> </div>
<CardContent className="space-y-3 md:space-y-4 px-3 md:px-6 pb-3 md:pb-6"> <div className="space-y-3 md:space-y-4">
<div className="space-y-1.5 md:space-y-2"> <div className="space-y-1.5 md:space-y-2">
<Label <Label
htmlFor="custom-instructions-settings" htmlFor="custom-instructions-settings"
@ -174,8 +175,8 @@ export function PromptConfigManager({ searchSpaceId }: PromptConfigManagerProps)
</AlertDescription> </AlertDescription>
</Alert> </Alert>
)} )}
</CardContent> </div>
</Card> </div>
{/* Action Buttons */} {/* Action Buttons */}
<div className="flex justify-end pt-3 md:pt-4"> <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[] }) { function PermissionsBadge({ permissions }: { permissions: string[] }) {
if (permissions.includes("*")) { if (permissions.includes("*")) {
return ( return (
<div className="px-2.5 py-1 rounded-md bg-muted/50 border border-border/60 text-muted-foreground"> <div className="rounded-md border-0 bg-muted px-1.5 py-0.5 text-muted-foreground">
<span className="text-xs font-medium whitespace-nowrap">Full access</span> <span className="text-[10px] font-medium whitespace-nowrap">Full access</span>
</div> </div>
); );
} }
return ( return (
<div className="px-2.5 py-1 rounded-md border border-border/60 bg-muted/50 text-muted-foreground"> <div className="rounded-md border-0 bg-muted px-1.5 py-0.5 text-muted-foreground">
<span className="text-xs font-medium whitespace-nowrap"> <span className="text-[10px] font-medium whitespace-nowrap">
{permissions.length} permissions {permissions.length} permissions
</span> </span>
</div> </div>

View file

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