Merge branch 'zhujingyi' into 'master'
Zhujingyi See merge request knowledge-base-bj/deerflow-server!76
This commit is contained in:
commit
d40ee80680
23 files changed
+594
-269
No files matched your search
@@ -1,6 +1,6 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { Slot } from "@radix-ui/react-slot";
|
import { Slot } from "@radix-ui/react-slot";
|
||||||
import { cva, type VariantProps } from "class-variance-authority";
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ const buttonVariants = cva(
|
|||||||
destructive:
|
destructive:
|
||||||
"cursor-pointer bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
"cursor-pointer bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||||
outline:
|
outline:
|
||||||
"cursor-pointer border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
"cursor-pointer border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-[#213D5E] dark:bg-[#022248] dark:text-[#CCD2DA] dark:hover:border-[#213D5E] dark:hover:bg-[#0a2f58] dark:hover:text-[#CCD2DA] dark:[&_svg]:text-[#CCD2DA]",
|
||||||
secondary:
|
secondary:
|
||||||
"cursor-pointer bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
"cursor-pointer bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||||
ghost:
|
ghost:
|
||||||
@@ -37,27 +37,27 @@ const buttonVariants = cva(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const Button = React.forwardRef<
|
const Button = React.forwardRef<
|
||||||
HTMLButtonElement,
|
HTMLButtonElement,
|
||||||
React.ComponentProps<"button"> &
|
React.ComponentProps<"button"> &
|
||||||
VariantProps<typeof buttonVariants> & {
|
VariantProps<typeof buttonVariants> & {
|
||||||
asChild?: boolean;
|
asChild?: boolean;
|
||||||
}
|
}
|
||||||
>(({ className, variant = "default", size = "default", asChild = false, ...props }, ref) => {
|
>(({ className, variant = "default", size = "default", asChild = false, ...props }, ref) => {
|
||||||
const Comp = asChild ? Slot : "button";
|
const Comp = asChild ? Slot : "button";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Comp
|
<Comp
|
||||||
ref={ref}
|
ref={ref}
|
||||||
data-slot="button"
|
data-slot="button"
|
||||||
{...(variant !== undefined && { "data-variant": variant })}
|
{...(variant !== undefined && { "data-variant": variant })}
|
||||||
{...(size !== undefined && { "data-size": size })}
|
{...(size !== undefined && { "data-size": size })}
|
||||||
className={cn(buttonVariants({ variant, size, className }))}
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
Button.displayName = "Button";
|
Button.displayName = "Button";
|
||||||
|
|
||||||
export { Button, buttonVariants };
|
export { Button, buttonVariants };
|
||||||
@@ -34,7 +34,7 @@ const emptyMediaVariants = cva(
|
|||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default: "bg-transparent",
|
default: "bg-transparent",
|
||||||
icon: "bg-muted text-foreground flex size-10 shrink-0 items-center justify-center rounded-lg [&_svg:not([class*='size-'])]:size-6",
|
icon: "bg-muted text-foreground flex size-10 shrink-0 items-center justify-center rounded-lg [&_svg:not([class*='size-'])]:size-6 dark:text-[#55A6FE] dark:bg-[#022248]",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
@@ -62,7 +62,7 @@ function EmptyTitle({ className, ...props }: React.ComponentProps<"div">) {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="empty-title"
|
data-slot="empty-title"
|
||||||
className={cn("text-lg font-medium tracking-tight", className)}
|
className={cn("text-lg font-medium tracking-tight dark:text-[#AACEFF]", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -73,7 +73,7 @@ function EmptyDescription({ className, ...props }: React.ComponentProps<"p">) {
|
|||||||
<div
|
<div
|
||||||
data-slot="empty-description"
|
data-slot="empty-description"
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-muted-foreground [&>a:hover]:text-primary text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4",
|
"text-muted-foreground [&>a:hover]:text-primary text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4 dark:text-[#55A6FE]",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -3,6 +3,14 @@ import * as React from "react";
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||||
|
const usesDarkNativePicker =
|
||||||
|
type === "number" ||
|
||||||
|
type === "date" ||
|
||||||
|
type === "datetime-local" ||
|
||||||
|
type === "time" ||
|
||||||
|
type === "month" ||
|
||||||
|
type === "week";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<input
|
<input
|
||||||
type={type}
|
type={type}
|
||||||
@@ -10,7 +18,8 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
|||||||
className={cn(
|
className={cn(
|
||||||
"h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base outline-none transition-[color,box-shadow] md:text-sm",
|
"h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base outline-none transition-[color,box-shadow] md:text-sm",
|
||||||
"border-input placeholder:text-muted-foreground",
|
"border-input placeholder:text-muted-foreground",
|
||||||
"dark:border-[#1B477E] dark:bg-[#022248] dark:text-[#AACEFF] dark:placeholder:text-[#566B7F] rounded-[4px] text-[16px]",
|
"dark:border-[#1B477E] dark:bg-[#0D2D54] dark:text-[#AACEFF] dark:placeholder:text-[#566B7F] dark:rounded-[4px] dark:text-[16px] md:dark:text-[16px]",
|
||||||
|
usesDarkNativePicker && "dark:[color-scheme:dark]",
|
||||||
"file:text-foreground selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
|
"file:text-foreground selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] dark:focus-visible:border-[#3b82f6] dark:focus-visible:ring-[#3b82f6]/30",
|
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] dark:focus-visible:border-[#3b82f6] dark:focus-visible:ring-[#3b82f6]/30",
|
||||||
"aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40",
|
"aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40",
|
||||||
|
|||||||
@@ -24,69 +24,70 @@ function SelectValue({
|
|||||||
return <SelectPrimitive.Value data-slot="select-value" {...props} />;
|
return <SelectPrimitive.Value data-slot="select-value" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SelectTrigger = React.forwardRef<
|
const SelectTrigger = React.forwardRef<
|
||||||
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
||||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> & {
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> & {
|
||||||
size?: "sm" | "default";
|
size?: "sm" | "default";
|
||||||
}
|
}
|
||||||
>(({ className, size = "default", children, ...props }, ref) => {
|
>(({ className, size = "default", children, ...props }, ref) => {
|
||||||
return (
|
return (
|
||||||
<SelectPrimitive.Trigger
|
<SelectPrimitive.Trigger
|
||||||
ref={ref}
|
ref={ref}
|
||||||
data-slot="select-trigger"
|
data-slot="select-trigger"
|
||||||
data-size={size}
|
data-size={size}
|
||||||
className={cn(
|
className={cn(
|
||||||
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit cursor-pointer items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive flex w-fit cursor-pointer items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
className,
|
"dark:border-[#1B477E] dark:bg-[#022248] dark:text-[#AACEE2] dark:data-[placeholder]:text-[#566B7F] dark:hover:bg-[#022248] dark:[&_svg:not([class*='text-'])]:text-[#AACEFF] dark:focus-visible:border-[#3b82f6] dark:focus-visible:ring-[#3b82f6]/30",
|
||||||
)}
|
className,
|
||||||
{...props}
|
)}
|
||||||
>
|
{...props}
|
||||||
{children}
|
>
|
||||||
<SelectPrimitive.Icon asChild>
|
{children}
|
||||||
<ChevronDownIcon className="size-4 opacity-50" />
|
<SelectPrimitive.Icon asChild>
|
||||||
</SelectPrimitive.Icon>
|
<ChevronDownIcon className="size-4 opacity-50" />
|
||||||
</SelectPrimitive.Trigger>
|
</SelectPrimitive.Icon>
|
||||||
);
|
</SelectPrimitive.Trigger>
|
||||||
});
|
);
|
||||||
|
});
|
||||||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
|
|
||||||
|
|
||||||
const SelectContent = React.forwardRef<
|
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
|
||||||
React.ElementRef<typeof SelectPrimitive.Content>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
const SelectContent = React.forwardRef<
|
||||||
>(({ className, children, position = "item-aligned", align = "center", ...props }, ref) => {
|
React.ElementRef<typeof SelectPrimitive.Content>,
|
||||||
return (
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
||||||
<SelectPrimitive.Portal>
|
>(({ className, children, position = "item-aligned", align = "center", ...props }, ref) => {
|
||||||
<SelectPrimitive.Content
|
return (
|
||||||
ref={ref}
|
<SelectPrimitive.Portal>
|
||||||
data-slot="select-content"
|
<SelectPrimitive.Content
|
||||||
className={cn(
|
ref={ref}
|
||||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
|
data-slot="select-content"
|
||||||
position === "popper" &&
|
className={cn(
|
||||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md dark:border-[#1B477E] dark:bg-[#022248] dark:text-[#AACEFF]",
|
||||||
className,
|
position === "popper" &&
|
||||||
)}
|
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||||
position={position}
|
className,
|
||||||
align={align}
|
)}
|
||||||
{...props}
|
position={position}
|
||||||
>
|
align={align}
|
||||||
<SelectScrollUpButton />
|
{...props}
|
||||||
<SelectPrimitive.Viewport
|
>
|
||||||
className={cn(
|
<SelectScrollUpButton />
|
||||||
"p-1",
|
<SelectPrimitive.Viewport
|
||||||
position === "popper" &&
|
className={cn(
|
||||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1",
|
"p-1",
|
||||||
)}
|
position === "popper" &&
|
||||||
>
|
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1",
|
||||||
{children}
|
)}
|
||||||
</SelectPrimitive.Viewport>
|
>
|
||||||
<SelectScrollDownButton />
|
{children}
|
||||||
</SelectPrimitive.Content>
|
</SelectPrimitive.Viewport>
|
||||||
</SelectPrimitive.Portal>
|
<SelectScrollDownButton />
|
||||||
);
|
</SelectPrimitive.Content>
|
||||||
});
|
</SelectPrimitive.Portal>
|
||||||
|
);
|
||||||
SelectContent.displayName = SelectPrimitive.Content.displayName;
|
});
|
||||||
|
|
||||||
|
SelectContent.displayName = SelectPrimitive.Content.displayName;
|
||||||
|
|
||||||
function SelectLabel({
|
function SelectLabel({
|
||||||
className,
|
className,
|
||||||
@@ -95,40 +96,44 @@ function SelectLabel({
|
|||||||
return (
|
return (
|
||||||
<SelectPrimitive.Label
|
<SelectPrimitive.Label
|
||||||
data-slot="select-label"
|
data-slot="select-label"
|
||||||
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
|
className={cn(
|
||||||
|
"text-muted-foreground px-2 py-1.5 text-xs dark:text-[#566B7F]",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const SelectItem = React.forwardRef<
|
const SelectItem = React.forwardRef<
|
||||||
React.ElementRef<typeof SelectPrimitive.Item>,
|
React.ElementRef<typeof SelectPrimitive.Item>,
|
||||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
||||||
>(({ className, children, ...props }, ref) => {
|
>(({ className, children, ...props }, ref) => {
|
||||||
return (
|
return (
|
||||||
<SelectPrimitive.Item
|
<SelectPrimitive.Item
|
||||||
ref={ref}
|
ref={ref}
|
||||||
data-slot="select-item"
|
data-slot="select-item"
|
||||||
className={cn(
|
className={cn(
|
||||||
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-pointer items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-pointer items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
||||||
className,
|
"dark:text-[#AACEFF] dark:focus:bg-[#0062D9] dark:focus:text-white dark:data-[highlighted]:bg-[#0062D9] dark:data-[highlighted]:text-white dark:[&_svg:not([class*='text-'])]:text-[#AACEFF] dark:focus:[&_svg:not([class*='text-'])]:text-white dark:data-[highlighted]:[&_svg:not([class*='text-'])]:text-white",
|
||||||
)}
|
className,
|
||||||
{...props}
|
)}
|
||||||
>
|
{...props}
|
||||||
<span
|
>
|
||||||
data-slot="select-item-indicator"
|
<span
|
||||||
className="absolute right-2 flex size-3.5 items-center justify-center"
|
data-slot="select-item-indicator"
|
||||||
>
|
className="absolute right-2 flex size-3.5 items-center justify-center"
|
||||||
<SelectPrimitive.ItemIndicator>
|
>
|
||||||
<CheckIcon className="size-4" />
|
<SelectPrimitive.ItemIndicator>
|
||||||
</SelectPrimitive.ItemIndicator>
|
<CheckIcon className="size-4" />
|
||||||
</span>
|
</SelectPrimitive.ItemIndicator>
|
||||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
</span>
|
||||||
</SelectPrimitive.Item>
|
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||||
);
|
</SelectPrimitive.Item>
|
||||||
});
|
);
|
||||||
|
});
|
||||||
SelectItem.displayName = SelectPrimitive.Item.displayName;
|
|
||||||
|
SelectItem.displayName = SelectPrimitive.Item.displayName;
|
||||||
|
|
||||||
function SelectSeparator({
|
function SelectSeparator({
|
||||||
className,
|
className,
|
||||||
@@ -151,7 +156,7 @@ function SelectScrollUpButton({
|
|||||||
<SelectPrimitive.ScrollUpButton
|
<SelectPrimitive.ScrollUpButton
|
||||||
data-slot="select-scroll-up-button"
|
data-slot="select-scroll-up-button"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex cursor-default items-center justify-center py-1",
|
"flex cursor-default items-center justify-center py-1 dark:text-[#AACEFF]",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -169,7 +174,7 @@ function SelectScrollDownButton({
|
|||||||
<SelectPrimitive.ScrollDownButton
|
<SelectPrimitive.ScrollDownButton
|
||||||
data-slot="select-scroll-down-button"
|
data-slot="select-scroll-down-button"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex cursor-default items-center justify-center py-1",
|
"flex cursor-default items-center justify-center py-1 dark:text-[#AACEFF]",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ function Switch({
|
|||||||
<SwitchPrimitive.Root
|
<SwitchPrimitive.Root
|
||||||
data-slot="switch"
|
data-slot="switch"
|
||||||
className={cn(
|
className={cn(
|
||||||
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-[#55A6FE] dark:data-[state=checked]:bg-[#0062D9] inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ const tabsListVariants = cva(
|
|||||||
variant: {
|
variant: {
|
||||||
default:
|
default:
|
||||||
"bg-muted dark:group-data-[orientation=horizontal]/tabs:h-8! dark:items-stretch dark:gap-0 dark:overflow-hidden dark:rounded-[2px] dark:border dark:border-[#55A6FE] dark:bg-[#022248] dark:p-0",
|
"bg-muted dark:group-data-[orientation=horizontal]/tabs:h-8! dark:items-stretch dark:gap-0 dark:overflow-hidden dark:rounded-[2px] dark:border dark:border-[#55A6FE] dark:bg-[#022248] dark:p-0",
|
||||||
line: "gap-1 bg-transparent",
|
line:
|
||||||
|
"gap-1 bg-transparent dark:group-data-[orientation=horizontal]/tabs:h-8! dark:items-stretch dark:gap-0 dark:overflow-visible dark:rounded-[2px] dark:border dark:border-[#55A6FE] dark:bg-[#022248] dark:p-0",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
@@ -70,13 +71,16 @@ const TabsTrigger = React.forwardRef<
|
|||||||
className={cn(
|
className={cn(
|
||||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring text-foreground/60 hover:text-foreground relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] outline-none group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring text-foreground/60 hover:text-foreground relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] outline-none group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
"dark:text-muted-foreground dark:hover:text-foreground",
|
"dark:text-muted-foreground dark:hover:text-foreground",
|
||||||
"group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:border-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent",
|
"group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent",
|
||||||
"data-[state=active]:bg-background data-[state=active]:text-foreground",
|
"data-[state=active]:bg-background data-[state=active]:text-foreground",
|
||||||
"dark:group-data-[variant=line]/tabs-list:data-[state=active]:border-input dark:group-data-[variant=line]/tabs-list:data-[state=active]:bg-input/30 dark:group-data-[variant=line]/tabs-list:data-[state=active]:text-foreground",
|
"dark:group-data-[variant=line]/tabs-list:relative dark:group-data-[variant=line]/tabs-list:!h-full dark:group-data-[variant=line]/tabs-list:self-stretch dark:group-data-[variant=line]/tabs-list:flex-none dark:group-data-[variant=line]/tabs-list:rounded-none dark:group-data-[variant=line]/tabs-list:border-0 dark:group-data-[variant=line]/tabs-list:bg-transparent dark:group-data-[variant=line]/tabs-list:px-4 dark:group-data-[variant=line]/tabs-list:!py-0 dark:group-data-[variant=line]/tabs-list:!leading-none dark:group-data-[variant=line]/tabs-list:text-[#CCD2DA] dark:group-data-[variant=line]/tabs-list:shadow-none dark:group-data-[variant=line]/tabs-list:hover:text-[#CCD2DA] dark:group-data-[variant=line]/tabs-list:focus-visible:ring-0 dark:group-data-[variant=line]/tabs-list:[&_svg]:text-[#CCD2DA]",
|
||||||
"dark:group-data-[variant=default]/tabs-list:relative dark:group-data-[variant=default]/tabs-list:!h-full dark:group-data-[variant=default]/tabs-list:self-stretch dark:group-data-[variant=default]/tabs-list:flex-none dark:group-data-[variant=default]/tabs-list:rounded-none dark:group-data-[variant=default]/tabs-list:border-0 dark:group-data-[variant=default]/tabs-list:bg-transparent dark:group-data-[variant=default]/tabs-list:px-4 dark:group-data-[variant=default]/tabs-list:!py-0 dark:group-data-[variant=default]/tabs-list:!leading-none dark:group-data-[variant=default]/tabs-list:shadow-none dark:group-data-[variant=default]/tabs-list:focus-visible:ring-0 dark:group-data-[variant=default]/tabs-list:[&_svg]:text-[#CCD2DA]",
|
"dark:group-data-[variant=line]/tabs-list:not(:first-child):border-l dark:group-data-[variant=line]/tabs-list:not(:first-child):border-[#1d3961] dark:group-data-[variant=line]/tabs-list:data-[state=active]:border-l-0",
|
||||||
|
"dark:group-data-[variant=line]/tabs-list:data-[state=active]:rounded-none dark:group-data-[variant=line]/tabs-list:data-[state=active]:border-0 dark:group-data-[variant=line]/tabs-list:data-[state=active]:bg-[#0062D9] dark:group-data-[variant=line]/tabs-list:data-[state=active]:text-[#FFFFFF] dark:group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none dark:group-data-[variant=line]/tabs-list:data-[state=active]:[&_svg]:text-[#FFFFFF]",
|
||||||
|
"dark:group-data-[variant=default]/tabs-list:flex dark:group-data-[variant=default]/tabs-list:min-h-0 dark:group-data-[variant=default]/tabs-list:!h-full dark:group-data-[variant=default]/tabs-list:flex-1 dark:group-data-[variant=default]/tabs-list:self-stretch dark:group-data-[variant=default]/tabs-list:rounded-none dark:group-data-[variant=default]/tabs-list:border-0 dark:group-data-[variant=default]/tabs-list:bg-transparent dark:group-data-[variant=default]/tabs-list:px-4 dark:group-data-[variant=default]/tabs-list:!py-0 dark:group-data-[variant=default]/tabs-list:!leading-none dark:group-data-[variant=default]/tabs-list:text-[#CCD2DA] dark:group-data-[variant=default]/tabs-list:shadow-none dark:group-data-[variant=default]/tabs-list:hover:text-[#CCD2DA] dark:group-data-[variant=default]/tabs-list:focus-visible:ring-0 dark:group-data-[variant=default]/tabs-list:[&_svg]:text-[#CCD2DA]",
|
||||||
"dark:group-data-[variant=default]/tabs-list:not(:first-child):border-l dark:group-data-[variant=default]/tabs-list:not(:first-child):border-[#1d3961] dark:group-data-[variant=default]/tabs-list:data-[state=active]:border-l-0",
|
"dark:group-data-[variant=default]/tabs-list:not(:first-child):border-l dark:group-data-[variant=default]/tabs-list:not(:first-child):border-[#1d3961] dark:group-data-[variant=default]/tabs-list:data-[state=active]:border-l-0",
|
||||||
"dark:group-data-[variant=default]/tabs-list:data-[state=active]:rounded-none dark:group-data-[variant=default]/tabs-list:data-[state=active]:border-0 dark:group-data-[variant=default]/tabs-list:data-[state=active]:bg-[#0062D9] dark:group-data-[variant=default]/tabs-list:data-[state=active]:text-[#FFFFFF] dark:group-data-[variant=default]/tabs-list:data-[state=active]:shadow-none dark:group-data-[variant=default]/tabs-list:data-[state=active]:[&_svg]:text-[#FFFFFF] dark:group-data-[variant=default]/tabs-list:hover:text-[#CCD2DA]",
|
"dark:group-data-[variant=default]/tabs-list:data-[state=active]:rounded-none dark:group-data-[variant=default]/tabs-list:data-[state=active]:border-0 dark:group-data-[variant=default]/tabs-list:data-[state=active]:bg-[#0062D9] dark:group-data-[variant=default]/tabs-list:data-[state=active]:text-[#FFFFFF] dark:group-data-[variant=default]/tabs-list:data-[state=active]:shadow-none dark:group-data-[variant=default]/tabs-list:data-[state=active]:hover:text-[#FFFFFF] dark:group-data-[variant=default]/tabs-list:data-[state=active]:[&_svg]:text-[#FFFFFF]",
|
||||||
"after:bg-foreground after:absolute after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-[state=active]:after:opacity-100",
|
"group-data-[variant=line]/tabs-list:after:pointer-events-none group-data-[variant=line]/tabs-list:after:absolute group-data-[variant=line]/tabs-list:after:inset-x-2 group-data-[variant=line]/tabs-list:after:bottom-[-5px] group-data-[variant=line]/tabs-list:after:h-0.5 group-data-[variant=line]/tabs-list:after:bg-foreground group-data-[variant=line]/tabs-list:after:opacity-0 group-data-[variant=line]/tabs-list:after:transition-opacity group-data-[variant=line]/tabs-list:after:content-[''] group-data-[variant=line]/tabs-list:data-[state=active]:after:opacity-100",
|
||||||
|
"dark:group-data-[variant=line]/tabs-list:after:inset-x-0 dark:group-data-[variant=line]/tabs-list:after:bottom-0 dark:group-data-[variant=line]/tabs-list:after:z-[2] dark:group-data-[variant=line]/tabs-list:after:h-[2px] dark:group-data-[variant=line]/tabs-list:after:bg-[#FFFFFF]",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { toast } from "sonner";
|
|||||||
|
|
||||||
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 { useTheme } from "@/contexts/ThemeContext";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
import {
|
import {
|
||||||
@@ -90,7 +91,7 @@ function V1SectionField({
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-xs font-medium text-muted-foreground">{label}</span>
|
<span className="text-xs font-medium text-muted-foreground dark:text-[#AACEE2]">{label}</span>
|
||||||
{!editing && (
|
{!editing && (
|
||||||
<Button size="icon" variant="ghost" className="size-6" onClick={() => { setDraft(value); setEditing(true); }}>
|
<Button size="icon" variant="ghost" className="size-6" onClick={() => { setDraft(value); setEditing(true); }}>
|
||||||
<Pencil className="size-3" />
|
<Pencil className="size-3" />
|
||||||
@@ -154,7 +155,7 @@ function V1FactItem({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rounded border p-2 space-y-1 group">
|
<div className="rounded border p-2 space-y-1 group ">
|
||||||
{editing ? (
|
{editing ? (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Textarea value={draftContent} onChange={(e) => setDraftContent(e.target.value)} className="min-h-[64px] text-sm" autoFocus />
|
<Textarea value={draftContent} onChange={(e) => setDraftContent(e.target.value)} className="min-h-[64px] text-sm" autoFocus />
|
||||||
@@ -196,7 +197,7 @@ function V1FactItem({
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm whitespace-pre-wrap">{String(fact.content ?? "")}</p>
|
<p className="text-sm whitespace-pre-wrap dark:text-[#AACEE2]">{String(fact.content ?? "")}</p>
|
||||||
{fact.sourceError ? <p className="text-xs text-red-600 bg-red-50 rounded px-2 py-1">避免:{String(fact.sourceError)}</p> : null}
|
{fact.sourceError ? <p className="text-xs text-red-600 bg-red-50 rounded px-2 py-1">避免:{String(fact.sourceError)}</p> : null}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -206,6 +207,7 @@ function V1FactItem({
|
|||||||
|
|
||||||
// 新增事实表单
|
// 新增事实表单
|
||||||
function V1AddFactForm({ agent, userId }: { agent: string | null; userId: string }) {
|
function V1AddFactForm({ agent, userId }: { agent: string | null; userId: string }) {
|
||||||
|
const { theme } = useTheme();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [content, setContent] = useState("");
|
const [content, setContent] = useState("");
|
||||||
const [category, setCategory] = useState("context");
|
const [category, setCategory] = useState("context");
|
||||||
@@ -222,7 +224,12 @@ function V1AddFactForm({ agent, userId }: { agent: string | null; userId: string
|
|||||||
|
|
||||||
if (!open) {
|
if (!open) {
|
||||||
return (
|
return (
|
||||||
<Button variant="outline" size="sm" className="mt-1 w-full" onClick={() => setOpen(true)}>
|
<Button
|
||||||
|
variant={theme === "dark" ? "default" : "outline"}
|
||||||
|
size="sm"
|
||||||
|
className="mt-1 w-full"
|
||||||
|
onClick={() => setOpen(true)}
|
||||||
|
>
|
||||||
<Plus className="size-3" />新增事实
|
<Plus className="size-3" />新增事实
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
@@ -276,8 +283,8 @@ export function V1BucketView({
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* 用户画像字段 */}
|
{/* 用户画像字段 */}
|
||||||
<div className="rounded-md border p-3 space-y-3">
|
<div className="rounded-md border p-3 space-y-3 dark:border-[#213D5E] dark:bg-[#02244C] dark:text-white">
|
||||||
<h4 className="text-xs font-semibold text-muted-foreground">用户画像</h4>
|
<h4 className="text-xs font-semibold text-muted-foreground dark:text-white">用户画像</h4>
|
||||||
{allCtxFields.map((field) => (
|
{allCtxFields.map((field) => (
|
||||||
<V1SectionField
|
<V1SectionField
|
||||||
key={field}
|
key={field}
|
||||||
@@ -291,8 +298,8 @@ export function V1BucketView({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 历史摘要字段 */}
|
{/* 历史摘要字段 */}
|
||||||
<div className="rounded-md border p-3 space-y-3">
|
<div className="rounded-md border p-3 space-y-3 dark:border-[#213D5E] dark:bg-[#02244C] dark:text-white">
|
||||||
<h4 className="text-xs font-semibold text-muted-foreground">历史摘要</h4>
|
<h4 className="text-xs font-semibold text-muted-foreground dark:text-white">历史摘要</h4>
|
||||||
{allHistoryFields.map((field) => (
|
{allHistoryFields.map((field) => (
|
||||||
<V1SectionField
|
<V1SectionField
|
||||||
key={field}
|
key={field}
|
||||||
@@ -306,8 +313,8 @@ export function V1BucketView({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 事实记录 */}
|
{/* 事实记录 */}
|
||||||
<div className="rounded-md border p-3 space-y-2">
|
<div className="rounded-md border p-3 space-y-2 dark:border-[#213D5E] dark:bg-[#022248]">
|
||||||
<h4 className="text-xs font-semibold text-muted-foreground">事实记录({factsRaw.length} 条)</h4>
|
<h4 className="text-xs font-semibold text-muted-foreground dark:text-white">事实记录({factsRaw.length} 条)</h4>
|
||||||
{factsRaw.map((fact, i) => (
|
{factsRaw.map((fact, i) => (
|
||||||
<V1FactItem key={String(fact.id ?? i)} fact={fact} agent={agent} userId={userId} />
|
<V1FactItem key={String(fact.id ?? i)} fact={fact} agent={agent} userId={userId} />
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -179,7 +179,7 @@ function UserAuditPanel({ userId }: { userId: string }) {
|
|||||||
<ScrollArea className="min-h-0 flex-1">
|
<ScrollArea className="min-h-0 flex-1">
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
{!threadId && (
|
{!threadId && (
|
||||||
<div className="text-muted-foreground flex flex-col items-center gap-2 py-16 text-sm">
|
<div className="text-muted-foreground flex flex-col items-center gap-2 py-16 text-sm dark:text-[#AACEF8]">
|
||||||
<MessageSquareIcon className="size-6" />
|
<MessageSquareIcon className="size-6" />
|
||||||
请从左侧选择一个会话
|
请从左侧选择一个会话
|
||||||
</div>
|
</div>
|
||||||
@@ -253,7 +253,7 @@ function AgentMemoryPanel({ userId }: { userId: string }) {
|
|||||||
<SelectTrigger className="h-8 w-48" size="sm">
|
<SelectTrigger className="h-8 w-48" size="sm">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent position="popper" side="bottom" sideOffset={4}>
|
||||||
{/* 默认助手始终显示,对应全局记忆文件 */}
|
{/* 默认助手始终显示,对应全局记忆文件 */}
|
||||||
<SelectItem value="default">默认助手</SelectItem>
|
<SelectItem value="default">默认助手</SelectItem>
|
||||||
{otherBuckets.map((b) => (
|
{otherBuckets.map((b) => (
|
||||||
|
|||||||
@@ -224,16 +224,16 @@ export function TagManagementPage() {
|
|||||||
<table className="w-full text-sm">
|
<table className="w-full text-sm">
|
||||||
<thead className="bg-muted/50">
|
<thead className="bg-muted/50">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="text-muted-foreground px-4 py-3 text-left font-medium">
|
<th className="text-muted-foreground px-4 py-3 text-left font-medium dark:text-white">
|
||||||
{t.adminTags.name}
|
{t.adminTags.name}
|
||||||
</th>
|
</th>
|
||||||
<th className="text-muted-foreground px-4 py-3 text-left font-medium">
|
<th className="text-muted-foreground px-4 py-3 text-left font-medium dark:text-white">
|
||||||
{t.adminTags.tagDescription}
|
{t.adminTags.tagDescription}
|
||||||
</th>
|
</th>
|
||||||
<th className="text-muted-foreground px-4 py-3 text-right font-medium">
|
<th className="text-muted-foreground px-4 py-3 text-right font-medium dark:text-white">
|
||||||
{t.adminTags.usageCount}
|
{t.adminTags.usageCount}
|
||||||
</th>
|
</th>
|
||||||
<th className="text-muted-foreground px-4 py-3 text-right font-medium">
|
<th className="text-muted-foreground px-4 py-3 text-right font-medium dark:text-white">
|
||||||
{t.adminTags.actions}
|
{t.adminTags.actions}
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Download, Loader2 } from "lucide-react";
|
import { AlertCircle,CheckCircle2, Download, Loader2 } from "lucide-react";
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
@@ -29,6 +29,7 @@ import {
|
|||||||
} from "@/core/tool-metrics/api";
|
} from "@/core/tool-metrics/api";
|
||||||
import { useToolMetrics, useToolMetricsSummary } from "@/core/tool-metrics/hooks";
|
import { useToolMetrics, useToolMetricsSummary } from "@/core/tool-metrics/hooks";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { useTheme } from "@/contexts/ThemeContext";
|
||||||
|
|
||||||
const PAGE_SIZE = 50;
|
const PAGE_SIZE = 50;
|
||||||
|
|
||||||
@@ -66,6 +67,7 @@ function categoryLabel(category?: string | null): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ToolMetricsPanel() {
|
export function ToolMetricsPanel() {
|
||||||
|
const { theme } = useTheme();
|
||||||
const [page, setPage] = useState(0);
|
const [page, setPage] = useState(0);
|
||||||
const [tool, setTool] = useState("");
|
const [tool, setTool] = useState("");
|
||||||
const [user, setUser] = useState("");
|
const [user, setUser] = useState("");
|
||||||
@@ -79,6 +81,31 @@ export function ToolMetricsPanel() {
|
|||||||
const [until, setUntil] = useState("");
|
const [until, setUntil] = useState("");
|
||||||
const [exporting, setExporting] = useState(false);
|
const [exporting, setExporting] = useState(false);
|
||||||
const [detailRow, setDetailRow] = useState<ToolMetric | null>(null);
|
const [detailRow, setDetailRow] = useState<ToolMetric | null>(null);
|
||||||
|
/** 成功 toast:仅暗色模式,浅色走 Sonner 默认 */
|
||||||
|
const successToastDarkOptions = {
|
||||||
|
classNames: {
|
||||||
|
toast:
|
||||||
|
"!rounded-[2px] !border !border-[#106455] !bg-[#052934] !text-[#FFFFFF] shadow-md",
|
||||||
|
title: "!text-[#FFFFFF]",
|
||||||
|
description: "!text-[#FFFFFF]",
|
||||||
|
},
|
||||||
|
icon: <CheckCircle2 className="size-4 shrink-0 text-[#2FD69A]" />,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/** 失败 toast:仅暗色模式,浅色走 Sonner 默认 */
|
||||||
|
const errorToastDarkOptions = {
|
||||||
|
classNames: {
|
||||||
|
toast:
|
||||||
|
"!rounded-[2px] !border !border-[#7F1A2A] !bg-[#311629] !text-[#FFFFFF] shadow-md",
|
||||||
|
title: "!text-[#FFFFFF]",
|
||||||
|
description: "!text-[#FFFFFF]",
|
||||||
|
},
|
||||||
|
icon: <AlertCircle className="size-4 shrink-0 text-[#F5222D]" />,
|
||||||
|
} as const;
|
||||||
|
const darkToastOpts =
|
||||||
|
theme === "dark"
|
||||||
|
? { success: successToastDarkOptions, error: errorToastDarkOptions }
|
||||||
|
: { success: undefined, error: undefined };
|
||||||
|
|
||||||
const filters = useMemo<ToolMetricsFilters>(() => {
|
const filters = useMemo<ToolMetricsFilters>(() => {
|
||||||
const out: ToolMetricsFilters = {};
|
const out: ToolMetricsFilters = {};
|
||||||
@@ -117,9 +144,9 @@ export function ToolMetricsPanel() {
|
|||||||
setExporting(true);
|
setExporting(true);
|
||||||
try {
|
try {
|
||||||
await downloadToolMetricsExcel(filters);
|
await downloadToolMetricsExcel(filters);
|
||||||
toast.success("已导出为 Excel");
|
toast.success("已导出为 Excel",darkToastOpts.success);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast.error(e instanceof Error ? e.message : "导出失败");
|
toast.error(e instanceof Error ? e.message : "导出失败",darkToastOpts.error);
|
||||||
} finally {
|
} finally {
|
||||||
setExporting(false);
|
setExporting(false);
|
||||||
}
|
}
|
||||||
@@ -167,7 +194,7 @@ export function ToolMetricsPanel() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Summary strip */}
|
{/* Summary strip */}
|
||||||
<div className="flex flex-wrap items-center gap-2 rounded-xl border bg-white p-4">
|
<div className="flex flex-wrap items-center gap-2 rounded-xl border bg-white p-4 dark:border-[#213D5E] dark:bg-[#02244C] dark:text-white">
|
||||||
<div className="mr-4 flex flex-col">
|
<div className="mr-4 flex flex-col">
|
||||||
<span className="text-muted-foreground text-xs">总调用</span>
|
<span className="text-muted-foreground text-xs">总调用</span>
|
||||||
<span className="text-lg font-semibold tabular-nums">
|
<span className="text-lg font-semibold tabular-nums">
|
||||||
@@ -213,7 +240,7 @@ export function ToolMetricsPanel() {
|
|||||||
|
|
||||||
{/* Per-skill breakdown — answers "was skill X invoked, and did it succeed". */}
|
{/* Per-skill breakdown — answers "was skill X invoked, and did it succeed". */}
|
||||||
{summary && summary.by_skill.length > 0 && (
|
{summary && summary.by_skill.length > 0 && (
|
||||||
<div className="rounded-xl border bg-white p-4">
|
<div className="rounded-xl border bg-white p-4 ">
|
||||||
<h3 className="mb-2 text-sm font-medium">各技能调用情况</h3>
|
<h3 className="mb-2 text-sm font-medium">各技能调用情况</h3>
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<table className="w-full text-sm">
|
<table className="w-full text-sm">
|
||||||
@@ -258,7 +285,7 @@ export function ToolMetricsPanel() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{summary && kind === "skill" && summary.by_skill.length === 0 && (
|
{summary && kind === "skill" && summary.by_skill.length === 0 && (
|
||||||
<div className="text-muted-foreground rounded-xl border bg-white p-4 text-sm">
|
<div className="text-muted-foreground rounded-xl border bg-white p-4 text-sm dark:border-[#213D5E] dark:bg-[#02244C] dark:text-white">
|
||||||
当前筛选范围内没有检测到任何技能调用记录。技能只有在被 Agent 通过
|
当前筛选范围内没有检测到任何技能调用记录。技能只有在被 Agent 通过
|
||||||
<code className="bg-muted mx-1 rounded px-1">skill_view</code>
|
<code className="bg-muted mx-1 rounded px-1">skill_view</code>
|
||||||
读取(即真正"调用")时才会被记录;仅在系统提示里被列出、但未被读取的技能不会计入。
|
读取(即真正"调用")时才会被记录;仅在系统提示里被列出、但未被读取的技能不会计入。
|
||||||
@@ -266,9 +293,9 @@ export function ToolMetricsPanel() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Filters */}
|
{/* Filters */}
|
||||||
<div className="grid gap-3 rounded-xl border bg-white p-4 md:grid-cols-3 lg:grid-cols-6">
|
<div className="grid gap-3 rounded-xl border bg-white p-4 md:grid-cols-3 lg:grid-cols-6 dark:border-[#213D5E] dark:bg-[#02244C] dark:text-white">
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<label className="text-muted-foreground text-xs">调用类型</label>
|
<label className="text-muted-foreground text-xs dark:text-white">调用类型</label>
|
||||||
<Select
|
<Select
|
||||||
value={kind}
|
value={kind}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
@@ -276,17 +303,17 @@ export function ToolMetricsPanel() {
|
|||||||
setPage(0);
|
setPage(0);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger className="dark:bg-[#0D2D54] dark:hover:bg-[#0D2D54]">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent position="popper" side="bottom" sideOffset={4}>
|
||||||
<SelectItem value="skill">技能(Agent Skill)</SelectItem>
|
<SelectItem value="skill">技能(Agent Skill)</SelectItem>
|
||||||
<SelectItem value="all">全部工具调用</SelectItem>
|
<SelectItem value="all">全部工具调用</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<label className="text-muted-foreground text-xs">技能名称</label>
|
<label className="text-muted-foreground text-xs dark:text-white">技能名称</label>
|
||||||
<Input
|
<Input
|
||||||
value={skill}
|
value={skill}
|
||||||
placeholder="按技能名筛选"
|
placeholder="按技能名筛选"
|
||||||
@@ -297,7 +324,7 @@ export function ToolMetricsPanel() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<label className="text-muted-foreground text-xs">工具名</label>
|
<label className="text-muted-foreground text-xs dark:text-white">工具名</label>
|
||||||
<Input
|
<Input
|
||||||
value={tool}
|
value={tool}
|
||||||
placeholder="按工具名筛选"
|
placeholder="按工具名筛选"
|
||||||
@@ -308,7 +335,7 @@ export function ToolMetricsPanel() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<label className="text-muted-foreground text-xs">用户名</label>
|
<label className="text-muted-foreground text-xs dark:text-white">用户名</label>
|
||||||
<Input
|
<Input
|
||||||
value={user}
|
value={user}
|
||||||
placeholder="@ 前部分"
|
placeholder="@ 前部分"
|
||||||
@@ -319,7 +346,7 @@ export function ToolMetricsPanel() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<label className="text-muted-foreground text-xs">状态</label>
|
<label className="text-muted-foreground text-xs dark:text-white">状态</label>
|
||||||
<Select
|
<Select
|
||||||
value={status}
|
value={status}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
@@ -327,10 +354,10 @@ export function ToolMetricsPanel() {
|
|||||||
setPage(0);
|
setPage(0);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger className="w-full dark:bg-[#0D2D54] dark:hover:bg-[#0D2D54]">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent position="popper" side="bottom" sideOffset={4}>
|
||||||
<SelectItem value="all">全部</SelectItem>
|
<SelectItem value="all">全部</SelectItem>
|
||||||
<SelectItem value="success">成功</SelectItem>
|
<SelectItem value="success">成功</SelectItem>
|
||||||
<SelectItem value="error">失败</SelectItem>
|
<SelectItem value="error">失败</SelectItem>
|
||||||
@@ -338,7 +365,7 @@ export function ToolMetricsPanel() {
|
|||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<label className="text-muted-foreground text-xs">错误类型</label>
|
<label className="text-muted-foreground text-xs dark:text-white">错误类型</label>
|
||||||
<Select
|
<Select
|
||||||
value={category}
|
value={category}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
@@ -346,10 +373,10 @@ export function ToolMetricsPanel() {
|
|||||||
setPage(0);
|
setPage(0);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger className="w-full dark:bg-[#0D2D54] dark:hover:bg-[#0D2D54]">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent position="popper" side="bottom" sideOffset={4}>
|
||||||
<SelectItem value="all">全部</SelectItem>
|
<SelectItem value="all">全部</SelectItem>
|
||||||
{TOOL_ERROR_CATEGORIES.map((cat) => (
|
{TOOL_ERROR_CATEGORIES.map((cat) => (
|
||||||
<SelectItem key={cat} value={cat}>
|
<SelectItem key={cat} value={cat}>
|
||||||
@@ -360,7 +387,7 @@ export function ToolMetricsPanel() {
|
|||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<label className="text-muted-foreground text-xs">开始时间</label>
|
<label className="text-muted-foreground text-xs dark:text-white">开始时间</label>
|
||||||
<Input
|
<Input
|
||||||
type="datetime-local"
|
type="datetime-local"
|
||||||
value={since}
|
value={since}
|
||||||
@@ -371,7 +398,7 @@ export function ToolMetricsPanel() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<label className="text-muted-foreground text-xs">结束时间</label>
|
<label className="text-muted-foreground text-xs dark:text-white">结束时间</label>
|
||||||
<Input
|
<Input
|
||||||
type="datetime-local"
|
type="datetime-local"
|
||||||
value={until}
|
value={until}
|
||||||
@@ -389,10 +416,10 @@ export function ToolMetricsPanel() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Table */}
|
{/* Table */}
|
||||||
<div className="rounded-xl border bg-white">
|
<div className="rounded-xl border bg-white dark:border-[#213D5E] dark:bg-[#02244C] dark:text-white">
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<table className="w-full min-w-[960px] text-sm">
|
<table className="w-full min-w-[960px] text-sm">
|
||||||
<thead className="bg-muted/60 text-muted-foreground">
|
<thead className="bg-muted/60 text-muted-foreground dark:text-white">
|
||||||
<tr>
|
<tr>
|
||||||
<Th>时间</Th>
|
<Th>时间</Th>
|
||||||
<Th>用户</Th>
|
<Th>用户</Th>
|
||||||
|
|||||||
@@ -229,7 +229,7 @@ export function MemorySettingsPage() {
|
|||||||
<SelectTrigger className="h-8 w-44" size="sm">
|
<SelectTrigger className="h-8 w-44" size="sm">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent position="popper" side="bottom" sideOffset={4}>
|
||||||
<SelectItem value="default">
|
<SelectItem value="default">
|
||||||
{m.workNotesDefaultAgent}
|
{m.workNotesDefaultAgent}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
@@ -490,7 +490,7 @@ function MemoryV1View({
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{/* 说明横幅 */}
|
{/* 说明横幅 */}
|
||||||
<div className="rounded-md border border-dashed px-4 py-3 text-sm text-muted-foreground">
|
<div className="rounded-md border border-dashed px-4 py-3 text-sm text-muted-foreground dark:border-[#213D5E] dark:bg-[#022248]">
|
||||||
V1 模式:记忆由系统在对话结束后自动提取,无需手动管理。
|
V1 模式:记忆由系统在对话结束后自动提取,无需手动管理。
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -509,13 +509,13 @@ function MemoryV1View({
|
|||||||
|
|
||||||
{/* 事实记录 Tab */}
|
{/* 事实记录 Tab */}
|
||||||
<TabsContent value="facts" className="mt-3">
|
<TabsContent value="facts" className="mt-3">
|
||||||
<div className="rounded-lg border p-4 space-y-3">
|
<div className="rounded-lg border p-4 space-y-3 dark:border-[#213D5E] dark:bg-[#022248]">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h3 className="text-base font-medium">事实记录</h3>
|
<h3 className="text-base font-medium">事实记录</h3>
|
||||||
<span className="text-xs text-muted-foreground">{memory.memory_usage}</span>
|
<span className="text-xs text-muted-foreground">{memory.memory_usage}</span>
|
||||||
</div>
|
</div>
|
||||||
{memory.facts.length === 0 ? (
|
{memory.facts.length === 0 ? (
|
||||||
<div className="rounded-md border border-dashed p-3 text-sm text-muted-foreground">
|
<div className="rounded-md border border-dashed p-3 text-sm text-muted-foreground ">
|
||||||
暂无事实记录,对话后系统会自动提取。
|
暂无事实记录,对话后系统会自动提取。
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -542,21 +542,21 @@ function MemoryV1View({
|
|||||||
</div>
|
</div>
|
||||||
<p className="text-sm whitespace-pre-wrap [overflow-wrap:anywhere]">{desensitize(fact.content)}</p>
|
<p className="text-sm whitespace-pre-wrap [overflow-wrap:anywhere]">{desensitize(fact.content)}</p>
|
||||||
{fact.sourceError && (
|
{fact.sourceError && (
|
||||||
<p className="text-xs text-red-600 bg-red-50 rounded px-2 py-1">
|
<p className="rounded bg-red-50 px-2 py-1 text-xs text-red-600 dark:bg-[#311629] dark:text-[#EE212C]">
|
||||||
避免:{desensitize(fact.sourceError)}
|
避免:{desensitize(fact.sourceError)}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
{/* 用户画像 & 历史摘要 Tab */}
|
{/* 用户画像 & 历史摘要 Tab */}
|
||||||
<TabsContent value="profile" className="mt-3 space-y-4">
|
<TabsContent value="profile" className="mt-3 space-y-4">
|
||||||
{/* 用户画像 (userContext) */}
|
{/* 用户画像 (userContext) */}
|
||||||
<div className="rounded-lg border p-4 space-y-2">
|
<div className="rounded-lg border p-4 space-y-2 dark:border-[#213D5E] dark:bg-[#022248]">
|
||||||
<h3 className="text-base font-medium">用户画像</h3>
|
<h3 className="text-base font-medium">用户画像</h3>
|
||||||
{ctxEntries.length === 0 ? (
|
{ctxEntries.length === 0 ? (
|
||||||
<p className="text-sm text-muted-foreground">暂无画像信息,对话后系统会自动归纳。</p>
|
<p className="text-sm text-muted-foreground">暂无画像信息,对话后系统会自动归纳。</p>
|
||||||
@@ -575,7 +575,7 @@ function MemoryV1View({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 历史摘要 (history) */}
|
{/* 历史摘要 (history) */}
|
||||||
<div className="rounded-lg border p-4 space-y-2">
|
<div className="rounded-lg border p-4 space-y-2 dark:border-[#213D5E] dark:bg-[#022248]">
|
||||||
<h3 className="text-base font-medium">历史摘要</h3>
|
<h3 className="text-base font-medium">历史摘要</h3>
|
||||||
{historyEntries.length === 0 ? (
|
{historyEntries.length === 0 ? (
|
||||||
<p className="text-sm text-muted-foreground">暂无历史摘要。</p>
|
<p className="text-sm text-muted-foreground">暂无历史摘要。</p>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
ClockIcon,
|
ClockIcon,
|
||||||
FileArchive,
|
FileArchive,
|
||||||
GlobeIcon,
|
GlobeIcon,
|
||||||
|
InfoIcon,
|
||||||
Loader2,
|
Loader2,
|
||||||
LockIcon,
|
LockIcon,
|
||||||
PinIcon,
|
PinIcon,
|
||||||
@@ -54,6 +55,7 @@ import { SkillDetailDialog } from "@/components/workspace/skills/skill-detail-di
|
|||||||
import { TagChipList } from "@/components/workspace/tags/tag-chip";
|
import { TagChipList } from "@/components/workspace/tags/tag-chip";
|
||||||
import { TagPickerDialog } from "@/components/workspace/tags/tag-picker-dialog";
|
import { TagPickerDialog } from "@/components/workspace/tags/tag-picker-dialog";
|
||||||
import { TagSearchBar } from "@/components/workspace/tags/tag-search-bar";
|
import { TagSearchBar } from "@/components/workspace/tags/tag-search-bar";
|
||||||
|
import { useTheme } from "@/contexts/ThemeContext";
|
||||||
import { getStoredAuth } from "@/core/auth";
|
import { getStoredAuth } from "@/core/auth";
|
||||||
import { useI18n } from "@/core/i18n/hooks";
|
import { useI18n } from "@/core/i18n/hooks";
|
||||||
import type { ValidateSkillUploadResponse } from "@/core/skills/api";
|
import type { ValidateSkillUploadResponse } from "@/core/skills/api";
|
||||||
@@ -88,8 +90,42 @@ export function SkillSettingsPage({ onClose }: { onClose?: () => void } = {}) {
|
|||||||
|
|
||||||
const PAGE_SIZE = 10;
|
const PAGE_SIZE = 10;
|
||||||
|
|
||||||
|
/** 重新扫描成功 toast:仅暗色模式使用,浅色走 Sonner 默认样式 */
|
||||||
|
const rescanSuccessToastDarkOptions = {
|
||||||
|
classNames: {
|
||||||
|
toast:
|
||||||
|
"!rounded-[2px] !border !border-[#106455] !bg-[#052934] !text-[#FFFFFF] shadow-md",
|
||||||
|
title: "!text-[#FFFFFF]",
|
||||||
|
description: "!text-[#FFFFFF]",
|
||||||
|
},
|
||||||
|
icon: <CheckCircle2 className="size-4 shrink-0 text-[#2FD69A]" />,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/** 重新扫描无变更 toast:仅暗色模式使用,浅色走 Sonner 默认样式 */
|
||||||
|
const rescanInfoToastDarkOptions = {
|
||||||
|
classNames: {
|
||||||
|
toast:
|
||||||
|
"!rounded-[2px] !border !border-[#704812] !bg-[#202222] !text-[#FFFFFF] shadow-md",
|
||||||
|
title: "!text-[#FFFFFF]",
|
||||||
|
description: "!text-[#FFFFFF]",
|
||||||
|
},
|
||||||
|
icon: <InfoIcon className="size-4 shrink-0 text-[#FF9900]" />,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/** 重新扫描失败 toast:仅暗色模式使用,浅色走 Sonner 默认样式 */
|
||||||
|
const rescanErrorToastDarkOptions = {
|
||||||
|
classNames: {
|
||||||
|
toast:
|
||||||
|
"!rounded-[2px] !border !border-[#7F1A2A] !bg-[#311629] !text-[#FFFFFF] shadow-md",
|
||||||
|
title: "!text-[#FFFFFF]",
|
||||||
|
description: "!text-[#FFFFFF]",
|
||||||
|
},
|
||||||
|
icon: <AlertCircle className="size-4 shrink-0 text-[#F5222D]" />,
|
||||||
|
} as const;
|
||||||
|
|
||||||
function SkillSettingsList({ onClose }: { onClose?: () => void }) {
|
function SkillSettingsList({ onClose }: { onClose?: () => void }) {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
const { theme } = useTheme();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [filter, setFilter] = useState<string>("mine");
|
const [filter, setFilter] = useState<string>("mine");
|
||||||
const [installOpen, setInstallOpen] = useState(false);
|
const [installOpen, setInstallOpen] = useState(false);
|
||||||
@@ -174,13 +210,21 @@ function SkillSettingsList({ onClose }: { onClose?: () => void }) {
|
|||||||
"{count}",
|
"{count}",
|
||||||
String(result.adopted.length),
|
String(result.adopted.length),
|
||||||
),
|
),
|
||||||
|
theme === "dark" ? rescanSuccessToastDarkOptions : undefined,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
toast.info(t.settings.skills.rescanSkillsNoop);
|
toast.info(
|
||||||
|
t.settings.skills.rescanSkillsNoop,
|
||||||
|
theme === "dark" ? rescanInfoToastDarkOptions : undefined,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error(err instanceof Error ? err.message : String(err));
|
toast.error(
|
||||||
|
err instanceof Error ? err.message : String(err),
|
||||||
|
theme === "dark" ? rescanErrorToastDarkOptions : undefined,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full flex-col gap-4">
|
<div className="flex w-full flex-col gap-4">
|
||||||
@@ -398,7 +442,7 @@ export function SkillRow({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Item className="w-full" variant="outline">
|
<Item className="w-full dark:border-[#213D5E] dark:bg-[#02244C] dark:text-white" variant="outline">
|
||||||
<ItemContent
|
<ItemContent
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
onClick={() => setDetailOpen(true)}
|
onClick={() => setDetailOpen(true)}
|
||||||
|
|||||||
@@ -425,10 +425,15 @@ function ConfigCard() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="max-w-[20rem]">
|
<SelectTrigger
|
||||||
|
className={cn(
|
||||||
|
"max-w-[20rem]",
|
||||||
|
"dark:border-[#1B477E] dark:bg-[#0D2D54] dark:text-[#AACEFF] dark:data-[placeholder]:text-[#566B7F] dark:hover:bg-[#0D2D54]",
|
||||||
|
)}
|
||||||
|
>
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent position="popper" side="bottom" sideOffset={4}>
|
||||||
<SelectItem value={DEFAULT_MODEL_VALUE}>
|
<SelectItem value={DEFAULT_MODEL_VALUE}>
|
||||||
{tt.curatorModelDefault}
|
{tt.curatorModelDefault}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
|
|||||||
@@ -9,16 +9,16 @@ function DraftWaitingPlaceholder() {
|
|||||||
return (
|
return (
|
||||||
<div className="h-full flex flex-col items-center justify-center gap-5 select-none">
|
<div className="h-full flex flex-col items-center justify-center gap-5 select-none">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="w-16 h-16 rounded-2xl bg-muted/60 flex items-center justify-center">
|
<div className="w-16 h-16 rounded-2xl bg-muted/60 flex items-center justify-center dark:bg-[#022248] ">
|
||||||
<FileText className="w-8 h-8 text-muted-foreground/40" />
|
<FileText className="w-8 h-8 text-muted-foreground/40 dark:text-[#55A6FE] " />
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute -bottom-1.5 -right-1.5 w-6 h-6 rounded-full bg-primary/10 flex items-center justify-center">
|
<div className="absolute -bottom-1.5 -right-1.5 w-6 h-6 rounded-full bg-primary/10 flex items-center justify-center dark:bg-[#102D5C] ">
|
||||||
<PenLine className="w-3.5 h-3.5 text-primary/60" />
|
<PenLine className="w-3.5 h-3.5 text-primary/60 dark:text-[#2580FF] " />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center space-y-1.5 max-w-[200px]">
|
<div className="text-center space-y-1.5 max-w-[200px]">
|
||||||
<p className="text-sm font-medium text-foreground/60">草稿将在此处显示</p>
|
<p className="text-sm font-medium text-foreground/60 dark:text-[#AACEFF]">草稿将在此处显示</p>
|
||||||
<p className="text-[11px] text-muted-foreground leading-relaxed">
|
<p className="text-[11px] text-muted-foreground leading-relaxed dark:text-[#55A6FE]">
|
||||||
完成写作流程后,AI 生成的内容将实时出现在这里
|
完成写作流程后,AI 生成的内容将实时出现在这里
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -25,7 +25,9 @@ interface AIWritingFormProps {
|
|||||||
const INPUT_CLASS =
|
const INPUT_CLASS =
|
||||||
'flex h-12 w-full rounded-xl border border-[#e8dedd] bg-white px-4 py-2 text-sm shadow-sm ' +
|
'flex h-12 w-full rounded-xl border border-[#e8dedd] bg-white px-4 py-2 text-sm shadow-sm ' +
|
||||||
'placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 ' +
|
'placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 ' +
|
||||||
'focus-visible:ring-ring disabled:opacity-50';
|
'focus-visible:ring-ring disabled:opacity-50 ' +
|
||||||
|
'dark:border-[#1B477E] dark:bg-[#022248] dark:text-[#AACEFF] dark:placeholder:text-[#566B7F] ' +
|
||||||
|
'dark:[color-scheme:dark] dark:focus-visible:border-[#3b82f6] dark:focus-visible:ring-[#3b82f6]/30';
|
||||||
|
|
||||||
const DEFAULT_KEYWORD_COUNT = 1;
|
const DEFAULT_KEYWORD_COUNT = 1;
|
||||||
const DEFAULT_AUDIENCE = '普通读者';
|
const DEFAULT_AUDIENCE = '普通读者';
|
||||||
@@ -127,16 +129,18 @@ export function AIWritingForm({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit} className="space-y-4 px-4 py-4">
|
<form onSubmit={handleSubmit} className="space-y-4 px-4 py-4">
|
||||||
<div className="rounded-2xl border border-[#ebe3e2] bg-[#fcf8f8] p-4">
|
<div className="rounded-2xl border border-[#ebe3e2] bg-[#fcf8f8] p-4 dark:border-[#213D5E] dark:bg-[#02244C] dark:text-white [&_h3]:dark:text-white [&_label]:dark:text-white">
|
||||||
<div className="flex items-center gap-2 border-b border-[#ece5e4] pb-3">
|
<div className="flex items-center gap-2 border-b border-[#ece5e4] pb-3 dark:border-[#213D5E]">
|
||||||
<PenLine className="size-4 text-[#8b6f6f]" />
|
<PenLine className="size-4 text-[#8b6f6f] dark:text-white" />
|
||||||
<h3 className="text-[18px] font-semibold leading-none text-[#2f2f2f]">写作配置选项</h3>
|
<h3 className="text-[18px] font-semibold leading-none text-[#2f2f2f] dark:text-white">
|
||||||
|
写作配置选项
|
||||||
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-4 space-y-2">
|
<div className="mt-4 space-y-2">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<label className={FIELD_LABEL_CLASS}>写作意图</label>
|
<label className={FIELD_LABEL_CLASS}>写作意图</label>
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground dark:text-white/70">
|
||||||
{intent.length} / {MAX_INTENT_LENGTH} 字
|
{intent.length} / {MAX_INTENT_LENGTH} 字
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -162,7 +166,7 @@ export function AIWritingForm({
|
|||||||
<SelectTrigger className={FIELD_TRIGGER_CLASS}>
|
<SelectTrigger className={FIELD_TRIGGER_CLASS}>
|
||||||
<SelectValue placeholder={typesLoading ? '加载中…' : '选择类型'} />
|
<SelectValue placeholder={typesLoading ? '加载中…' : '选择类型'} />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent position="popper" side="bottom" sideOffset={4}>
|
||||||
{articleTypes.map((t) => (
|
{articleTypes.map((t) => (
|
||||||
<SelectItem key={t.key} value={t.key} className="text-sm">
|
<SelectItem key={t.key} value={t.key} className="text-sm">
|
||||||
{t.label}
|
{t.label}
|
||||||
@@ -229,7 +233,7 @@ export function AIWritingForm({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent position="popper" side="bottom" sideOffset={4}>
|
||||||
{models.map((m) => (
|
{models.map((m) => (
|
||||||
<SelectItem key={m.name} value={m.name} className="text-sm">
|
<SelectItem key={m.name} value={m.name} className="text-sm">
|
||||||
{m.display_name || m.name}
|
{m.display_name || m.name}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { BotIcon, RefreshCwIcon, SearchIcon } from "lucide-react";
|
import { AlertCircle, BotIcon, CheckCircle2, RefreshCwIcon, SearchIcon } from "lucide-react";
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
@@ -22,9 +22,37 @@ import {
|
|||||||
import { AgentCard } from "@/components/workspace/agents/agent-card";
|
import { AgentCard } from "@/components/workspace/agents/agent-card";
|
||||||
import { useAdminSyncAgentSkills, useAllAgentsAdmin } from "@/core/agents";
|
import { useAdminSyncAgentSkills, useAllAgentsAdmin } from "@/core/agents";
|
||||||
import type { AgentSkillsSyncResult } from "@/core/agents/api";
|
import type { AgentSkillsSyncResult } from "@/core/agents/api";
|
||||||
|
import { useTheme } from "@/contexts/ThemeContext";
|
||||||
import { desensitize } from "@/lib/desensitize";
|
import { desensitize } from "@/lib/desensitize";
|
||||||
|
|
||||||
|
/** 成功 toast:仅暗色模式,浅色走 Sonner 默认 */
|
||||||
|
const successToastDarkOptions = {
|
||||||
|
classNames: {
|
||||||
|
toast:
|
||||||
|
"!rounded-[2px] !border !border-[#106455] !bg-[#052934] !text-[#FFFFFF] shadow-md",
|
||||||
|
title: "!text-[#FFFFFF]",
|
||||||
|
description: "!text-[#FFFFFF]",
|
||||||
|
},
|
||||||
|
icon: <CheckCircle2 className="size-4 shrink-0 text-[#2FD69A]" />,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/** 失败 toast:仅暗色模式,浅色走 Sonner 默认 */
|
||||||
|
const errorToastDarkOptions = {
|
||||||
|
classNames: {
|
||||||
|
toast:
|
||||||
|
"!rounded-[2px] !border !border-[#7F1A2A] !bg-[#311629] !text-[#FFFFFF] shadow-md",
|
||||||
|
title: "!text-[#FFFFFF]",
|
||||||
|
description: "!text-[#FFFFFF]",
|
||||||
|
},
|
||||||
|
icon: <AlertCircle className="size-4 shrink-0 text-[#F5222D]" />,
|
||||||
|
} as const;
|
||||||
|
|
||||||
export default function AdminAgentsPage() {
|
export default function AdminAgentsPage() {
|
||||||
|
const { theme } = useTheme();
|
||||||
|
const darkToastOpts =
|
||||||
|
theme === "dark"
|
||||||
|
? { success: successToastDarkOptions, error: errorToastDarkOptions }
|
||||||
|
: { success: undefined, error: undefined };
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const { agents, isLoading } = useAllAgentsAdmin(true);
|
const { agents, isLoading } = useAllAgentsAdmin(true);
|
||||||
const syncSkills = useAdminSyncAgentSkills();
|
const syncSkills = useAdminSyncAgentSkills();
|
||||||
@@ -39,14 +67,21 @@ export default function AdminAgentsPage() {
|
|||||||
setSyncResult(result);
|
setSyncResult(result);
|
||||||
setSyncDialogOpen(true);
|
setSyncDialogOpen(true);
|
||||||
if (result.modified === 0) {
|
if (result.modified === 0) {
|
||||||
toast.success("所有智能体的技能均有效,无需清理");
|
toast.success(
|
||||||
|
"所有智能体的技能均有效,无需清理",
|
||||||
|
darkToastOpts.success,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
toast.success(
|
toast.success(
|
||||||
`已清理 ${result.modified} 个智能体中的失效技能引用`,
|
`已清理 ${result.modified} 个智能体中的失效技能引用`,
|
||||||
|
darkToastOpts.success,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error(err instanceof Error ? err.message : String(err));
|
toast.error(
|
||||||
|
err instanceof Error ? err.message : String(err),
|
||||||
|
darkToastOpts.error,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { toast } from "sonner";
|
|||||||
|
|
||||||
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 { useTheme } from "@/contexts/ThemeContext";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -70,6 +71,7 @@ const emptyForm = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function AdminScheduledTasksPage() {
|
export default function AdminScheduledTasksPage() {
|
||||||
|
const { theme } = useTheme();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
// ?task= carries the task to re-select when returning from a run detail.
|
// ?task= carries the task to re-select when returning from a run detail.
|
||||||
@@ -227,7 +229,10 @@ export default function AdminScheduledTasksPage() {
|
|||||||
onChange={(event) => setSearchInput(event.target.value)}
|
onChange={(event) => setSearchInput(event.target.value)}
|
||||||
placeholder="按任务名称搜索"
|
placeholder="按任务名称搜索"
|
||||||
/>
|
/>
|
||||||
<Button type="submit" variant="outline">
|
<Button
|
||||||
|
type="submit"
|
||||||
|
variant={theme === "dark" ? "default" : "outline"}
|
||||||
|
>
|
||||||
<Search className="size-4" />
|
<Search className="size-4" />
|
||||||
搜索
|
搜索
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -84,10 +84,10 @@ export default function AdminSkillsPage() {
|
|||||||
)}
|
)}
|
||||||
{!isLoading && !error && filteredSkills.length === 0 && (
|
{!isLoading && !error && filteredSkills.length === 0 && (
|
||||||
<div className="flex h-64 flex-col items-center justify-center gap-3 text-center">
|
<div className="flex h-64 flex-col items-center justify-center gap-3 text-center">
|
||||||
<div className="bg-muted flex h-14 w-14 items-center justify-center rounded-full">
|
<div className="bg-muted flex h-14 w-14 items-center justify-center rounded-full dark:bg-[#022248]">
|
||||||
<SparklesIcon className="text-muted-foreground h-7 w-7" />
|
<SparklesIcon className="text-muted-foreground h-7 w-7 dark:text-[#AACEFF]" />
|
||||||
</div>
|
</div>
|
||||||
<p className="text-muted-foreground text-sm">
|
<p className="text-muted-foreground text-sm dark:text-[#AACEFF]">
|
||||||
当前没有可管理的技能。
|
当前没有可管理的技能。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Download, Loader2 } from "lucide-react";
|
import { AlertCircle,CheckCircle2, Download, Loader2 } from "lucide-react";
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
} from "@/core/llm-metrics/api";
|
} from "@/core/llm-metrics/api";
|
||||||
import { useLlmMetrics } from "@/core/llm-metrics/hooks";
|
import { useLlmMetrics } from "@/core/llm-metrics/hooks";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { useTheme } from "@/contexts/ThemeContext";
|
||||||
|
|
||||||
const PAGE_SIZE = 50;
|
const PAGE_SIZE = 50;
|
||||||
|
|
||||||
@@ -74,6 +75,7 @@ function speedColor(tps?: number | null): string {
|
|||||||
|
|
||||||
/** The original LLM-call metrics view, now one tab of the metrics page. */
|
/** The original LLM-call metrics view, now one tab of the metrics page. */
|
||||||
function LlmMetricsPanel() {
|
function LlmMetricsPanel() {
|
||||||
|
const { theme } = useTheme();
|
||||||
const [page, setPage] = useState(0);
|
const [page, setPage] = useState(0);
|
||||||
const [model, setModel] = useState("");
|
const [model, setModel] = useState("");
|
||||||
const [user, setUser] = useState("");
|
const [user, setUser] = useState("");
|
||||||
@@ -81,6 +83,31 @@ function LlmMetricsPanel() {
|
|||||||
const [since, setSince] = useState("");
|
const [since, setSince] = useState("");
|
||||||
const [until, setUntil] = useState("");
|
const [until, setUntil] = useState("");
|
||||||
const [exporting, setExporting] = useState(false);
|
const [exporting, setExporting] = useState(false);
|
||||||
|
/** 成功 toast:仅暗色模式,浅色走 Sonner 默认 */
|
||||||
|
const successToastDarkOptions = {
|
||||||
|
classNames: {
|
||||||
|
toast:
|
||||||
|
"!rounded-[2px] !border !border-[#106455] !bg-[#052934] !text-[#FFFFFF] shadow-md",
|
||||||
|
title: "!text-[#FFFFFF]",
|
||||||
|
description: "!text-[#FFFFFF]",
|
||||||
|
},
|
||||||
|
icon: <CheckCircle2 className="size-4 shrink-0 text-[#2FD69A]" />,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/** 失败 toast:仅暗色模式,浅色走 Sonner 默认 */
|
||||||
|
const errorToastDarkOptions = {
|
||||||
|
classNames: {
|
||||||
|
toast:
|
||||||
|
"!rounded-[2px] !border !border-[#7F1A2A] !bg-[#311629] !text-[#FFFFFF] shadow-md",
|
||||||
|
title: "!text-[#FFFFFF]",
|
||||||
|
description: "!text-[#FFFFFF]",
|
||||||
|
},
|
||||||
|
icon: <AlertCircle className="size-4 shrink-0 text-[#F5222D]" />,
|
||||||
|
} as const;
|
||||||
|
const darkToastOpts =
|
||||||
|
theme === "dark"
|
||||||
|
? { success: successToastDarkOptions, error: errorToastDarkOptions }
|
||||||
|
: { success: undefined, error: undefined };
|
||||||
|
|
||||||
const filters = useMemo<LlmMetricsFilters>(() => {
|
const filters = useMemo<LlmMetricsFilters>(() => {
|
||||||
const out: LlmMetricsFilters = {};
|
const out: LlmMetricsFilters = {};
|
||||||
@@ -103,9 +130,9 @@ function LlmMetricsPanel() {
|
|||||||
setExporting(true);
|
setExporting(true);
|
||||||
try {
|
try {
|
||||||
await downloadLlmMetricsExcel(filters);
|
await downloadLlmMetricsExcel(filters);
|
||||||
toast.success("已导出为 Excel");
|
toast.success("已导出为 Excel",darkToastOpts.success);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast.error(e instanceof Error ? e.message : "导出失败");
|
toast.error(e instanceof Error ? e.message : "导出失败",darkToastOpts.error);
|
||||||
} finally {
|
} finally {
|
||||||
setExporting(false);
|
setExporting(false);
|
||||||
}
|
}
|
||||||
@@ -267,9 +294,9 @@ function LlmMetricsPanel() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-3 rounded-xl border bg-white p-4 md:grid-cols-5">
|
<div className="grid gap-3 rounded-xl border bg-white p-4 md:grid-cols-5 dark:border-[#213D5E] dark:bg-[#02244C] dark:text-white">
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1 ">
|
||||||
<label className="text-muted-foreground text-xs">模型</label>
|
<label className="text-muted-foreground text-xs dark:text-white">模型</label>
|
||||||
<Input
|
<Input
|
||||||
value={model}
|
value={model}
|
||||||
placeholder="按模型名筛选"
|
placeholder="按模型名筛选"
|
||||||
@@ -280,7 +307,7 @@ function LlmMetricsPanel() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<label className="text-muted-foreground text-xs">用户名</label>
|
<label className="text-muted-foreground text-xs dark:text-white">用户名</label>
|
||||||
<Input
|
<Input
|
||||||
value={user}
|
value={user}
|
||||||
placeholder="按用户名匹配(@ 前部分)"
|
placeholder="按用户名匹配(@ 前部分)"
|
||||||
@@ -291,7 +318,7 @@ function LlmMetricsPanel() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<label className="text-muted-foreground text-xs">状态</label>
|
<label className="text-muted-foreground text-xs dark:text-white">状态</label>
|
||||||
<Select
|
<Select
|
||||||
value={status}
|
value={status}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
@@ -299,10 +326,10 @@ function LlmMetricsPanel() {
|
|||||||
setPage(0);
|
setPage(0);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger className="w-full dark:bg-[#0D2D54] dark:hover:bg-[#0D2D54]">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent position="popper" side="bottom" sideOffset={4}>
|
||||||
<SelectItem value="all">全部</SelectItem>
|
<SelectItem value="all">全部</SelectItem>
|
||||||
<SelectItem value="success">成功</SelectItem>
|
<SelectItem value="success">成功</SelectItem>
|
||||||
<SelectItem value="error">失败</SelectItem>
|
<SelectItem value="error">失败</SelectItem>
|
||||||
@@ -310,7 +337,7 @@ function LlmMetricsPanel() {
|
|||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<label className="text-muted-foreground text-xs">开始时间</label>
|
<label className="text-muted-foreground text-xs dark:text-white">开始时间</label>
|
||||||
<Input
|
<Input
|
||||||
type="datetime-local"
|
type="datetime-local"
|
||||||
value={since}
|
value={since}
|
||||||
@@ -321,7 +348,7 @@ function LlmMetricsPanel() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<label className="text-muted-foreground text-xs">结束时间</label>
|
<label className="text-muted-foreground text-xs dark:text-white">结束时间</label>
|
||||||
<Input
|
<Input
|
||||||
type="datetime-local"
|
type="datetime-local"
|
||||||
value={until}
|
value={until}
|
||||||
@@ -338,10 +365,10 @@ function LlmMetricsPanel() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="rounded-xl border bg-white">
|
<div className="rounded-xl border bg-white dark:border-[#213D5E] dark:bg-[#02244C] ">
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<table className="w-full min-w-[1080px] text-sm">
|
<table className="w-full min-w-[1080px] text-sm">
|
||||||
<thead className="bg-muted/60 text-muted-foreground">
|
<thead className="bg-muted/60 text-muted-foreground dark:text-white">
|
||||||
<tr>
|
<tr>
|
||||||
{visibleColumns.map((col) => (
|
{visibleColumns.map((col) => (
|
||||||
<Th
|
<Th
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Loader2Icon, ShieldIcon } from "lucide-react";
|
import { AlertCircle,CheckCircle2, Loader2Icon, ShieldIcon } from "lucide-react";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import { useTheme } from "@/contexts/ThemeContext";
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
@@ -12,13 +13,36 @@ import {
|
|||||||
useUpdatePrivateSquareSettings,
|
useUpdatePrivateSquareSettings,
|
||||||
} from "@/core/system-settings/hooks";
|
} from "@/core/system-settings/hooks";
|
||||||
|
|
||||||
|
/** 成功 toast:仅暗色模式,浅色走 Sonner 默认 */
|
||||||
|
const successToastDarkOptions = {
|
||||||
|
classNames: {
|
||||||
|
toast:
|
||||||
|
"!rounded-[2px] !border !border-[#106455] !bg-[#052934] !text-[#FFFFFF] shadow-md",
|
||||||
|
title: "!text-[#FFFFFF]",
|
||||||
|
description: "!text-[#FFFFFF]",
|
||||||
|
},
|
||||||
|
icon: <CheckCircle2 className="size-4 shrink-0 text-[#2FD69A]" />,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/** 失败 toast:仅暗色模式,浅色走 Sonner 默认 */
|
||||||
|
const errorToastDarkOptions = {
|
||||||
|
classNames: {
|
||||||
|
toast:
|
||||||
|
"!rounded-[2px] !border !border-[#7F1A2A] !bg-[#311629] !text-[#FFFFFF] shadow-md",
|
||||||
|
title: "!text-[#FFFFFF]",
|
||||||
|
description: "!text-[#FFFFFF]",
|
||||||
|
},
|
||||||
|
icon: <AlertCircle className="size-4 shrink-0 text-[#F5222D]" />,
|
||||||
|
} as const;
|
||||||
|
|
||||||
export default function PrivateSquareSettingsPage() {
|
export default function PrivateSquareSettingsPage() {
|
||||||
const { data, isLoading, error } = usePrivateSquareStatus();
|
const { data, isLoading, error } = usePrivateSquareStatus();
|
||||||
const update = useUpdatePrivateSquareSettings();
|
const update = useUpdatePrivateSquareSettings();
|
||||||
|
const { theme } = useTheme();
|
||||||
const [enabled, setEnabled] = useState(false);
|
const [enabled, setEnabled] = useState(false);
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
const [confirm, setConfirm] = useState("");
|
const [confirm, setConfirm] = useState("");
|
||||||
|
|
||||||
// The status response never contains the password itself, so re-syncing
|
// The status response never contains the password itself, so re-syncing
|
||||||
// ``enabled`` from a refetch wouldn't clobber any in-progress edit — but we
|
// ``enabled`` from a refetch wouldn't clobber any in-progress edit — but we
|
||||||
// still gate the seed on a one-shot ref to match the prompt-prefix page's
|
// still gate the seed on a one-shot ref to match the prompt-prefix page's
|
||||||
@@ -31,10 +55,14 @@ export default function PrivateSquareSettingsPage() {
|
|||||||
setEnabled(data.enabled);
|
setEnabled(data.enabled);
|
||||||
initializedRef.current = true;
|
initializedRef.current = true;
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
const darkToastOpts =
|
||||||
|
theme === "dark"
|
||||||
|
? { success: successToastDarkOptions, error: errorToastDarkOptions }
|
||||||
|
: { success: undefined, error: undefined };
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
if (password && password !== confirm) {
|
if (password && password !== confirm) {
|
||||||
toast.error("两次输入的密码不一致");
|
toast.error("两次输入的密码不一致", darkToastOpts.error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -48,9 +76,9 @@ export default function PrivateSquareSettingsPage() {
|
|||||||
});
|
});
|
||||||
setPassword("");
|
setPassword("");
|
||||||
setConfirm("");
|
setConfirm("");
|
||||||
toast.success("已保存");
|
toast.success("已保存",darkToastOpts.success);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast.error(e instanceof Error ? e.message : "保存失败");
|
toast.error(e instanceof Error ? e.message : "保存失败",darkToastOpts.error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -60,9 +88,9 @@ export default function PrivateSquareSettingsPage() {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await update.mutateAsync({ password: "" });
|
await update.mutateAsync({ password: "" });
|
||||||
toast.success("已清除密码");
|
toast.success("已清除密码",darkToastOpts.success);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast.error(e instanceof Error ? e.message : "清除失败");
|
toast.error(e instanceof Error ? e.message : "清除失败", darkToastOpts.error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -85,7 +113,7 @@ export default function PrivateSquareSettingsPage() {
|
|||||||
加载失败:{error.message}
|
加载失败:{error.message}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col gap-5 rounded-xl border bg-white px-5 py-5">
|
<div className="flex flex-col gap-5 rounded-xl border bg-white px-5 py-5 dark:border-[#213D5E] dark:bg-[#02244C] dark:text-white">
|
||||||
<div className="flex items-center justify-between gap-4">
|
<div className="flex items-center justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm font-medium">启用私密广场</div>
|
<div className="text-sm font-medium">启用私密广场</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Loader2Icon } from "lucide-react";
|
import { Loader2Icon,AlertCircle,CheckCircle2, } from "lucide-react";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
@@ -12,11 +12,33 @@ import {
|
|||||||
usePromptPrefixSettings,
|
usePromptPrefixSettings,
|
||||||
useUpdatePromptPrefixSettings,
|
useUpdatePromptPrefixSettings,
|
||||||
} from "@/core/system-settings/hooks";
|
} from "@/core/system-settings/hooks";
|
||||||
|
import { useTheme } from "@/contexts/ThemeContext";
|
||||||
|
|
||||||
|
/** 成功 toast:仅暗色模式,浅色走 Sonner 默认 */
|
||||||
|
const successToastDarkOptions = {
|
||||||
|
classNames: {
|
||||||
|
toast:
|
||||||
|
"!rounded-[2px] !border !border-[#106455] !bg-[#052934] !text-[#FFFFFF] shadow-md",
|
||||||
|
title: "!text-[#FFFFFF]",
|
||||||
|
description: "!text-[#FFFFFF]",
|
||||||
|
},
|
||||||
|
icon: <CheckCircle2 className="size-4 shrink-0 text-[#2FD69A]" />,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/** 失败 toast:仅暗色模式,浅色走 Sonner 默认 */
|
||||||
|
const errorToastDarkOptions = {
|
||||||
|
classNames: {
|
||||||
|
toast:
|
||||||
|
"!rounded-[2px] !border !border-[#7F1A2A] !bg-[#311629] !text-[#FFFFFF] shadow-md",
|
||||||
|
title: "!text-[#FFFFFF]",
|
||||||
|
description: "!text-[#FFFFFF]",
|
||||||
|
},
|
||||||
|
icon: <AlertCircle className="size-4 shrink-0 text-[#F5222D]" />,
|
||||||
|
} as const;
|
||||||
export default function PromptPrefixSettingsPage() {
|
export default function PromptPrefixSettingsPage() {
|
||||||
const { data, isLoading, error } = usePromptPrefixSettings();
|
const { data, isLoading, error } = usePromptPrefixSettings();
|
||||||
const update = useUpdatePromptPrefixSettings();
|
const update = useUpdatePromptPrefixSettings();
|
||||||
|
const { theme } = useTheme();
|
||||||
const [enabled, setEnabled] = useState(false);
|
const [enabled, setEnabled] = useState(false);
|
||||||
const [switchLabel, setSwitchLabel] = useState("");
|
const [switchLabel, setSwitchLabel] = useState("");
|
||||||
const [promptPrefix, setPromptPrefix] = useState("");
|
const [promptPrefix, setPromptPrefix] = useState("");
|
||||||
@@ -34,13 +56,18 @@ export default function PromptPrefixSettingsPage() {
|
|||||||
initializedRef.current = true;
|
initializedRef.current = true;
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
|
const darkToastOpts =
|
||||||
|
theme === "dark"
|
||||||
|
? { success: successToastDarkOptions, error: errorToastDarkOptions }
|
||||||
|
: { success: undefined, error: undefined };
|
||||||
|
|
||||||
if (enabled && !switchLabel.trim()) {
|
if (enabled && !switchLabel.trim()) {
|
||||||
toast.error("开启时必须填写开关名称");
|
toast.error("开启时必须填写开关名称",darkToastOpts.error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (enabled && !promptPrefix.trim()) {
|
if (enabled && !promptPrefix.trim()) {
|
||||||
toast.error("开启时必须填写拼接的前缀内容");
|
toast.error("开启时必须填写拼接的前缀内容",darkToastOpts.error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -49,9 +76,9 @@ export default function PromptPrefixSettingsPage() {
|
|||||||
switch_label: switchLabel,
|
switch_label: switchLabel,
|
||||||
prompt_prefix: promptPrefix,
|
prompt_prefix: promptPrefix,
|
||||||
});
|
});
|
||||||
toast.success("已保存");
|
toast.success("已保存",darkToastOpts.success);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast.error(e instanceof Error ? e.message : "保存失败");
|
toast.error(e instanceof Error ? e.message : "保存失败",darkToastOpts.error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -74,7 +101,7 @@ export default function PromptPrefixSettingsPage() {
|
|||||||
加载失败:{error.message}
|
加载失败:{error.message}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col gap-5 rounded-xl border bg-white px-5 py-5">
|
<div className="flex flex-col gap-5 rounded-xl border bg-white px-5 py-5 dark:border-[#213D5E] dark:bg-[#02244C] dark:text-white">
|
||||||
<div className="flex items-center justify-between gap-4">
|
<div className="flex items-center justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm font-medium">向用户暴露开关</div>
|
<div className="text-sm font-medium">向用户暴露开关</div>
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import {
|
import {
|
||||||
|
AlertCircle,
|
||||||
|
CheckCircle2,
|
||||||
GripVerticalIcon,
|
GripVerticalIcon,
|
||||||
Loader2Icon,
|
Loader2Icon,
|
||||||
PencilIcon,
|
PencilIcon,
|
||||||
@@ -11,6 +13,7 @@ import {
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { useTheme } from "@/contexts/ThemeContext";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -34,7 +37,30 @@ type DialogState =
|
|||||||
| { mode: "edit"; question: RecommendedQuestion }
|
| { mode: "edit"; question: RecommendedQuestion }
|
||||||
| null;
|
| null;
|
||||||
|
|
||||||
|
/** 成功 toast:仅暗色模式,浅色走 Sonner 默认 */
|
||||||
|
const successToastDarkOptions = {
|
||||||
|
classNames: {
|
||||||
|
toast:
|
||||||
|
"!rounded-[2px] !border !border-[#106455] !bg-[#052934] !text-[#FFFFFF] shadow-md",
|
||||||
|
title: "!text-[#FFFFFF]",
|
||||||
|
description: "!text-[#FFFFFF]",
|
||||||
|
},
|
||||||
|
icon: <CheckCircle2 className="size-4 shrink-0 text-[#2FD69A]" />,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/** 失败 toast:仅暗色模式,浅色走 Sonner 默认 */
|
||||||
|
const errorToastDarkOptions = {
|
||||||
|
classNames: {
|
||||||
|
toast:
|
||||||
|
"!rounded-[2px] !border !border-[#7F1A2A] !bg-[#311629] !text-[#FFFFFF] shadow-md",
|
||||||
|
title: "!text-[#FFFFFF]",
|
||||||
|
description: "!text-[#FFFFFF]",
|
||||||
|
},
|
||||||
|
icon: <AlertCircle className="size-4 shrink-0 text-[#F5222D]" />,
|
||||||
|
} as const;
|
||||||
|
|
||||||
export default function RecommendedQuestionsPage() {
|
export default function RecommendedQuestionsPage() {
|
||||||
|
const { theme } = useTheme();
|
||||||
const { questions, isLoading, error } = useRecommendedQuestions();
|
const { questions, isLoading, error } = useRecommendedQuestions();
|
||||||
const createMutation = useCreateRecommendedQuestion();
|
const createMutation = useCreateRecommendedQuestion();
|
||||||
const updateMutation = useUpdateRecommendedQuestion();
|
const updateMutation = useUpdateRecommendedQuestion();
|
||||||
@@ -70,7 +96,7 @@ export default function RecommendedQuestionsPage() {
|
|||||||
const title = formTitle.trim();
|
const title = formTitle.trim();
|
||||||
const content = formContent.trim();
|
const content = formContent.trim();
|
||||||
if (!title || !content) {
|
if (!title || !content) {
|
||||||
toast.error("标题和内容不能为空");
|
toast.error("标题和内容不能为空", darkToastOpts.error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,17 +107,20 @@ export default function RecommendedQuestionsPage() {
|
|||||||
content,
|
content,
|
||||||
sort_order: questions.length,
|
sort_order: questions.length,
|
||||||
});
|
});
|
||||||
toast.success("问题已添加");
|
toast.success("问题已添加", darkToastOpts.success);
|
||||||
} else if (dialog?.mode === "edit") {
|
} else if (dialog?.mode === "edit") {
|
||||||
await updateMutation.mutateAsync({
|
await updateMutation.mutateAsync({
|
||||||
id: dialog.question.id,
|
id: dialog.question.id,
|
||||||
body: { title, content },
|
body: { title, content },
|
||||||
});
|
});
|
||||||
toast.success("问题已更新");
|
toast.success("问题已更新", darkToastOpts.success);
|
||||||
}
|
}
|
||||||
closeDialog();
|
closeDialog();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast.error(e instanceof Error ? e.message : "操作失败");
|
toast.error(
|
||||||
|
e instanceof Error ? e.message : "操作失败",
|
||||||
|
darkToastOpts.error,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,9 +128,12 @@ export default function RecommendedQuestionsPage() {
|
|||||||
try {
|
try {
|
||||||
await deleteMutation.mutateAsync(q.id);
|
await deleteMutation.mutateAsync(q.id);
|
||||||
setDeleteTarget(null);
|
setDeleteTarget(null);
|
||||||
toast.success("问题已删除");
|
toast.success("问题已删除", darkToastOpts.success);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast.error(e instanceof Error ? e.message : "删除失败");
|
toast.error(
|
||||||
|
e instanceof Error ? e.message : "删除失败",
|
||||||
|
darkToastOpts.error,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,7 +160,7 @@ export default function RecommendedQuestionsPage() {
|
|||||||
try {
|
try {
|
||||||
await reorderMutation.mutateAsync(next.map((q) => q.id));
|
await reorderMutation.mutateAsync(next.map((q) => q.id));
|
||||||
} catch {
|
} catch {
|
||||||
toast.error("排序保存失败");
|
toast.error("排序保存失败", darkToastOpts.error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,6 +170,11 @@ export default function RecommendedQuestionsPage() {
|
|||||||
deleteMutation.isPending ||
|
deleteMutation.isPending ||
|
||||||
reorderMutation.isPending;
|
reorderMutation.isPending;
|
||||||
|
|
||||||
|
const darkToastOpts =
|
||||||
|
theme === "dark"
|
||||||
|
? { success: successToastDarkOptions, error: errorToastDarkOptions }
|
||||||
|
: { success: undefined, error: undefined };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto flex w-full max-w-2xl flex-col gap-6 px-6 py-10">
|
<div className="mx-auto flex w-full max-w-2xl flex-col gap-6 px-6 py-10">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
@@ -162,15 +199,15 @@ export default function RecommendedQuestionsPage() {
|
|||||||
<p>加载失败:{error.message}</p>
|
<p>加载失败:{error.message}</p>
|
||||||
</div>
|
</div>
|
||||||
) : questions.length === 0 ? (
|
) : questions.length === 0 ? (
|
||||||
<div className="text-muted-foreground flex flex-col items-center gap-3 rounded-xl border border-dashed py-16 text-sm">
|
<div className="text-muted-foreground flex flex-col items-center gap-3 rounded-xl border border-dashed py-16 text-sm dark:text-[#7FACF2] dark:!border-[#213F62]">
|
||||||
<p>暂无推荐问题</p>
|
<p>暂无推荐问题</p>
|
||||||
<Button variant="outline" size="sm" onClick={openAdd}>
|
<Button size="sm" onClick={openAdd}>
|
||||||
<PlusIcon className="mr-1.5 size-4" />
|
<PlusIcon className="mr-1.5 size-4" />
|
||||||
添加第一个问题
|
添加第一个问题
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2 ">
|
||||||
{questions.map((q, index) => (
|
{questions.map((q, index) => (
|
||||||
<div
|
<div
|
||||||
key={q.id}
|
key={q.id}
|
||||||
@@ -182,18 +219,18 @@ export default function RecommendedQuestionsPage() {
|
|||||||
setDragIndex(null);
|
setDragIndex(null);
|
||||||
setDragOverIndex(null);
|
setDragOverIndex(null);
|
||||||
}}
|
}}
|
||||||
className={`flex items-start gap-3 rounded-xl border bg-white px-4 py-3.5 transition-colors ${
|
className={`flex items-start gap-3 rounded-xl border bg-white px-4 py-3.5 transition-colors dark:bg-[#022248] dark:!border-[#213D5E] ${
|
||||||
dragOverIndex === index && dragIndex !== index
|
dragOverIndex === index && dragIndex !== index
|
||||||
? "border-blue-400 bg-blue-50"
|
? "border-blue-400 bg-blue-50 dark:border-[#2580FF] dark:bg-[#0D2D54]"
|
||||||
: "border-[#e4e4e7]"
|
: "border-[#e4e4e7]"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<GripVerticalIcon className="text-muted-foreground/40 mt-0.5 size-4 shrink-0 cursor-grab active:cursor-grabbing" />
|
<GripVerticalIcon className="text-muted-foreground/40 mt-0.5 size-4 shrink-0 cursor-grab active:cursor-grabbing dark:text-[#566B7F]" />
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<div className="truncate text-sm font-medium text-[#18181b]">
|
<div className="truncate text-sm font-medium text-[#18181b] dark:text-white">
|
||||||
{q.title}
|
{q.title}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-muted-foreground mt-0.5 line-clamp-2 text-xs">
|
<div className="text-muted-foreground mt-0.5 line-clamp-2 text-xs dark:text-[#7FACF2]">
|
||||||
{q.content}
|
{q.content}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,8 +2,10 @@
|
|||||||
|
|
||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import {
|
import {
|
||||||
|
AlertCircle,
|
||||||
BellOff,
|
BellOff,
|
||||||
BellRing,
|
BellRing,
|
||||||
|
CheckCircle2,
|
||||||
Download,
|
Download,
|
||||||
Edit3,
|
Edit3,
|
||||||
Eye,
|
Eye,
|
||||||
@@ -62,9 +64,32 @@ import {
|
|||||||
statusLabel,
|
statusLabel,
|
||||||
statusVariant,
|
statusVariant,
|
||||||
} from "@/components/workspace/scheduled-tasks/schedule-builder";
|
} from "@/components/workspace/scheduled-tasks/schedule-builder";
|
||||||
|
import { useTheme } from "@/contexts/ThemeContext";
|
||||||
import { desensitize } from "@/lib/desensitize";
|
import { desensitize } from "@/lib/desensitize";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
/** 成功 toast:仅暗色模式,浅色走 Sonner 默认 */
|
||||||
|
const successToastDarkOptions = {
|
||||||
|
classNames: {
|
||||||
|
toast:
|
||||||
|
"!rounded-[2px] !border !border-[#106455] !bg-[#052934] !text-[#FFFFFF] shadow-md",
|
||||||
|
title: "!text-[#FFFFFF]",
|
||||||
|
description: "!text-[#FFFFFF]",
|
||||||
|
},
|
||||||
|
icon: <CheckCircle2 className="size-4 shrink-0 text-[#2FD69A]" />,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/** 失败 toast:仅暗色模式,浅色走 Sonner 默认 */
|
||||||
|
const errorToastDarkOptions = {
|
||||||
|
classNames: {
|
||||||
|
toast:
|
||||||
|
"!rounded-[2px] !border !border-[#7F1A2A] !bg-[#311629] !text-[#FFFFFF] shadow-md",
|
||||||
|
title: "!text-[#FFFFFF]",
|
||||||
|
description: "!text-[#FFFFFF]",
|
||||||
|
},
|
||||||
|
icon: <AlertCircle className="size-4 shrink-0 text-[#F5222D]" />,
|
||||||
|
} as const;
|
||||||
|
|
||||||
function formatDate(value?: string | null) {
|
function formatDate(value?: string | null) {
|
||||||
return value ? new Date(value).toLocaleString() : "暂无";
|
return value ? new Date(value).toLocaleString() : "暂无";
|
||||||
}
|
}
|
||||||
@@ -88,6 +113,11 @@ const emptyForm = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function ScheduledTasksPage() {
|
export default function ScheduledTasksPage() {
|
||||||
|
const { theme } = useTheme();
|
||||||
|
const darkToastOpts =
|
||||||
|
theme === "dark"
|
||||||
|
? { success: successToastDarkOptions, error: errorToastDarkOptions }
|
||||||
|
: { success: undefined, error: undefined };
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
// When returning from a run-detail page the task id is carried in ?task=,
|
// When returning from a run-detail page the task id is carried in ?task=,
|
||||||
@@ -207,7 +237,7 @@ export default function ScheduledTasksPage() {
|
|||||||
require_markdown: form.require_markdown,
|
require_markdown: form.require_markdown,
|
||||||
});
|
});
|
||||||
setSelectedTaskId(task.task_id);
|
setSelectedTaskId(task.task_id);
|
||||||
toast.success("任务已创建");
|
toast.success("任务已创建", darkToastOpts.success);
|
||||||
} else if (editing) {
|
} else if (editing) {
|
||||||
await actions.update.mutateAsync({
|
await actions.update.mutateAsync({
|
||||||
taskId: editing.task_id,
|
taskId: editing.task_id,
|
||||||
@@ -221,11 +251,14 @@ export default function ScheduledTasksPage() {
|
|||||||
require_markdown: form.require_markdown,
|
require_markdown: form.require_markdown,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
toast.success("任务已更新");
|
toast.success("任务已更新", darkToastOpts.success);
|
||||||
}
|
}
|
||||||
closeDialog();
|
closeDialog();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(error instanceof Error ? error.message : "保存失败");
|
toast.error(
|
||||||
|
error instanceof Error ? error.message : "保存失败",
|
||||||
|
darkToastOpts.error,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -233,16 +266,22 @@ export default function ScheduledTasksPage() {
|
|||||||
try {
|
try {
|
||||||
await downloadScheduledTaskRun(runId);
|
await downloadScheduledTaskRun(runId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(error instanceof Error ? error.message : "下载失败");
|
toast.error(
|
||||||
|
error instanceof Error ? error.message : "下载失败",
|
||||||
|
darkToastOpts.error,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteRun = async (runId: string) => {
|
const handleDeleteRun = async (runId: string) => {
|
||||||
try {
|
try {
|
||||||
await actions.removeRun.mutateAsync(runId);
|
await actions.removeRun.mutateAsync(runId);
|
||||||
toast.success("执行记录已删除");
|
toast.success("执行记录已删除", darkToastOpts.success);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(error instanceof Error ? error.message : "删除失败");
|
toast.error(
|
||||||
|
error instanceof Error ? error.message : "删除失败",
|
||||||
|
darkToastOpts.error,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -273,14 +312,18 @@ export default function ScheduledTasksPage() {
|
|||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
className="dark:bg-[#0D2D54] dark:hover:!bg-[#2580FF] dark:hover:text-white"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
try {
|
try {
|
||||||
await actions.updateDeliveryProfile.mutateAsync({
|
await actions.updateDeliveryProfile.mutateAsync({
|
||||||
element_user_id: elementUserId || null,
|
element_user_id: elementUserId || null,
|
||||||
});
|
});
|
||||||
toast.success("Element 配置已保存");
|
toast.success("Element 配置已保存", darkToastOpts.success);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(error instanceof Error ? error.message : "保存失败");
|
toast.error(
|
||||||
|
error instanceof Error ? error.message : "保存失败",
|
||||||
|
darkToastOpts.error,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -437,7 +480,7 @@ export default function ScheduledTasksPage() {
|
|||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
actions.trigger.mutate(task.task_id);
|
actions.trigger.mutate(task.task_id);
|
||||||
toast.success("已触发执行");
|
toast.success("已触发执行", darkToastOpts.success);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<RefreshCw className="size-4" />
|
<RefreshCw className="size-4" />
|
||||||
@@ -451,13 +494,19 @@ export default function ScheduledTasksPage() {
|
|||||||
try {
|
try {
|
||||||
if (task.published) {
|
if (task.published) {
|
||||||
await actions.unpublish.mutateAsync(task.task_id);
|
await actions.unpublish.mutateAsync(task.task_id);
|
||||||
toast.success("已取消发布(已订阅者保留)");
|
toast.success(
|
||||||
|
"已取消发布(已订阅者保留)",
|
||||||
|
darkToastOpts.success,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
await actions.publish.mutateAsync(task.task_id);
|
await actions.publish.mutateAsync(task.task_id);
|
||||||
toast.success("已发布到广场");
|
toast.success("已发布到广场", darkToastOpts.success);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(error instanceof Error ? error.message : "操作失败");
|
toast.error(
|
||||||
|
error instanceof Error ? error.message : "操作失败",
|
||||||
|
darkToastOpts.error,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -488,9 +537,15 @@ export default function ScheduledTasksPage() {
|
|||||||
taskId: task.task_id,
|
taskId: task.task_id,
|
||||||
notifyElement: !notifyOn,
|
notifyElement: !notifyOn,
|
||||||
});
|
});
|
||||||
toast.success(notifyOn ? "已关闭 Element 通知" : "已开启 Element 通知");
|
toast.success(
|
||||||
|
notifyOn ? "已关闭 Element 通知" : "已开启 Element 通知",
|
||||||
|
darkToastOpts.success,
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(error instanceof Error ? error.message : "操作失败");
|
toast.error(
|
||||||
|
error instanceof Error ? error.message : "操作失败",
|
||||||
|
darkToastOpts.error,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -505,9 +560,12 @@ export default function ScheduledTasksPage() {
|
|||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
try {
|
try {
|
||||||
await actions.unsubscribe.mutateAsync(task.task_id);
|
await actions.unsubscribe.mutateAsync(task.task_id);
|
||||||
toast.success("已取消收藏");
|
toast.success("已取消收藏", darkToastOpts.success);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(error instanceof Error ? error.message : "操作失败");
|
toast.error(
|
||||||
|
error instanceof Error ? error.message : "操作失败",
|
||||||
|
darkToastOpts.error,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -581,7 +639,7 @@ export default function ScheduledTasksPage() {
|
|||||||
<SelectTrigger className="h-7 w-28 text-xs">
|
<SelectTrigger className="h-7 w-28 text-xs">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent position="popper" side="bottom" sideOffset={4}>
|
||||||
<SelectItem value="all">全部时间</SelectItem>
|
<SelectItem value="all">全部时间</SelectItem>
|
||||||
<SelectItem value="today">今天</SelectItem>
|
<SelectItem value="today">今天</SelectItem>
|
||||||
<SelectItem value="7d">近 7 天</SelectItem>
|
<SelectItem value="7d">近 7 天</SelectItem>
|
||||||
@@ -747,12 +805,15 @@ export default function ScheduledTasksPage() {
|
|||||||
taskId: subscribeTarget.task_id,
|
taskId: subscribeTarget.task_id,
|
||||||
notifyElement: subscribeNotify,
|
notifyElement: subscribeNotify,
|
||||||
});
|
});
|
||||||
toast.success("已收藏到我的任务");
|
toast.success("已收藏到我的任务", darkToastOpts.success);
|
||||||
setSubscribeTarget(null);
|
setSubscribeTarget(null);
|
||||||
setSubscribeNotify(false);
|
setSubscribeNotify(false);
|
||||||
setActiveTab("mine");
|
setActiveTab("mine");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(error instanceof Error ? error.message : "收藏失败");
|
toast.error(
|
||||||
|
error instanceof Error ? error.message : "收藏失败",
|
||||||
|
darkToastOpts.error,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
Reference in new issue
Block a user