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
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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" />
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue