@nextclaw/ui 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.cjs +28 -0
- package/CHANGELOG.md +7 -0
- package/dist/assets/index-BrN4G7FO.js +240 -0
- package/dist/assets/index-VjHB2nG6.css +1 -0
- package/dist/index.html +14 -0
- package/index.html +13 -0
- package/package.json +50 -0
- package/postcss.config.js +6 -0
- package/src/App.tsx +51 -0
- package/src/api/client.ts +40 -0
- package/src/api/config.ts +86 -0
- package/src/api/types.ts +78 -0
- package/src/api/websocket.ts +77 -0
- package/src/components/common/KeyValueEditor.tsx +65 -0
- package/src/components/common/MaskedInput.tsx +39 -0
- package/src/components/common/StatusBadge.tsx +56 -0
- package/src/components/common/TagInput.tsx +56 -0
- package/src/components/config/ChannelForm.tsx +259 -0
- package/src/components/config/ChannelsList.tsx +102 -0
- package/src/components/config/ModelConfig.tsx +181 -0
- package/src/components/config/ProviderForm.tsx +147 -0
- package/src/components/config/ProvidersList.tsx +90 -0
- package/src/components/config/UiConfig.tsx +189 -0
- package/src/components/layout/AppLayout.tsx +20 -0
- package/src/components/layout/Header.tsx +36 -0
- package/src/components/layout/Sidebar.tsx +103 -0
- package/src/components/ui/HighlightCard.tsx +40 -0
- package/src/components/ui/button.tsx +50 -0
- package/src/components/ui/card.tsx +78 -0
- package/src/components/ui/dialog.tsx +120 -0
- package/src/components/ui/input.tsx +23 -0
- package/src/components/ui/label.tsx +20 -0
- package/src/components/ui/scroll-area.tsx +21 -0
- package/src/components/ui/skeleton.tsx +15 -0
- package/src/components/ui/switch.tsx +37 -0
- package/src/components/ui/tabs-custom.tsx +45 -0
- package/src/components/ui/tabs.tsx +88 -0
- package/src/hooks/useConfig.ts +95 -0
- package/src/hooks/useWebSocket.ts +38 -0
- package/src/index.css +177 -0
- package/src/lib/i18n.ts +119 -0
- package/src/lib/utils.ts +6 -0
- package/src/main.tsx +10 -0
- package/src/stores/ui.store.ts +39 -0
- package/src/vite-env.d.ts +9 -0
- package/tailwind.config.js +43 -0
- package/tsconfig.json +18 -0
- package/vite.config.ts +25 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { cn } from '@/lib/utils';
|
|
3
|
+
|
|
4
|
+
const Card = React.forwardRef<
|
|
5
|
+
HTMLDivElement,
|
|
6
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
7
|
+
>(({ className, ...props }, ref) => (
|
|
8
|
+
<div
|
|
9
|
+
ref={ref}
|
|
10
|
+
className={cn(
|
|
11
|
+
'rounded-2xl border bg-card text-card-foreground shadow-sm transition-all duration-300 hover:shadow-premium',
|
|
12
|
+
className
|
|
13
|
+
)}
|
|
14
|
+
{...props}
|
|
15
|
+
/>
|
|
16
|
+
));
|
|
17
|
+
Card.displayName = 'Card';
|
|
18
|
+
|
|
19
|
+
const CardHeader = React.forwardRef<
|
|
20
|
+
HTMLDivElement,
|
|
21
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
22
|
+
>(({ className, ...props }, ref) => (
|
|
23
|
+
<div
|
|
24
|
+
ref={ref}
|
|
25
|
+
className={cn('flex flex-col space-y-1.5 p-6', className)}
|
|
26
|
+
{...props}
|
|
27
|
+
/>
|
|
28
|
+
));
|
|
29
|
+
CardHeader.displayName = 'CardHeader';
|
|
30
|
+
|
|
31
|
+
const CardTitle = React.forwardRef<
|
|
32
|
+
HTMLParagraphElement,
|
|
33
|
+
React.HTMLAttributes<HTMLHeadingElement>
|
|
34
|
+
>(({ className, ...props }, ref) => (
|
|
35
|
+
<h3
|
|
36
|
+
ref={ref}
|
|
37
|
+
className={cn(
|
|
38
|
+
'text-2xl font-semibold leading-none tracking-tight',
|
|
39
|
+
className
|
|
40
|
+
)}
|
|
41
|
+
{...props}
|
|
42
|
+
/>
|
|
43
|
+
));
|
|
44
|
+
CardTitle.displayName = 'CardTitle';
|
|
45
|
+
|
|
46
|
+
const CardDescription = React.forwardRef<
|
|
47
|
+
HTMLParagraphElement,
|
|
48
|
+
React.HTMLAttributes<HTMLParagraphElement>
|
|
49
|
+
>(({ className, ...props }, ref) => (
|
|
50
|
+
<p
|
|
51
|
+
ref={ref}
|
|
52
|
+
className={cn('text-sm text-muted-foreground', className)}
|
|
53
|
+
{...props}
|
|
54
|
+
/>
|
|
55
|
+
));
|
|
56
|
+
CardDescription.displayName = 'CardDescription';
|
|
57
|
+
|
|
58
|
+
const CardContent = React.forwardRef<
|
|
59
|
+
HTMLDivElement,
|
|
60
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
61
|
+
>(({ className, ...props }, ref) => (
|
|
62
|
+
<div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
|
|
63
|
+
));
|
|
64
|
+
CardContent.displayName = 'CardContent';
|
|
65
|
+
|
|
66
|
+
const CardFooter = React.forwardRef<
|
|
67
|
+
HTMLDivElement,
|
|
68
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
69
|
+
>(({ className, ...props }, ref) => (
|
|
70
|
+
<div
|
|
71
|
+
ref={ref}
|
|
72
|
+
className={cn('flex items-center p-6 pt-0', className)}
|
|
73
|
+
{...props}
|
|
74
|
+
/>
|
|
75
|
+
));
|
|
76
|
+
CardFooter.displayName = 'CardFooter';
|
|
77
|
+
|
|
78
|
+
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
|
3
|
+
import { X } from "lucide-react"
|
|
4
|
+
|
|
5
|
+
import { cn } from "@/lib/utils"
|
|
6
|
+
|
|
7
|
+
const Dialog = DialogPrimitive.Root
|
|
8
|
+
|
|
9
|
+
const DialogTrigger = DialogPrimitive.Trigger
|
|
10
|
+
|
|
11
|
+
const DialogPortal = DialogPrimitive.Portal
|
|
12
|
+
|
|
13
|
+
const DialogClose = DialogPrimitive.Close
|
|
14
|
+
|
|
15
|
+
const DialogOverlay = React.forwardRef<
|
|
16
|
+
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
|
17
|
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
|
18
|
+
>(({ className, ...props }, ref) => (
|
|
19
|
+
<DialogPrimitive.Overlay
|
|
20
|
+
ref={ref}
|
|
21
|
+
className={cn(
|
|
22
|
+
"fixed inset-0 z-50 bg-black/50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
|
23
|
+
className
|
|
24
|
+
)}
|
|
25
|
+
{...props}
|
|
26
|
+
/>
|
|
27
|
+
))
|
|
28
|
+
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
|
29
|
+
|
|
30
|
+
const DialogContent = React.forwardRef<
|
|
31
|
+
React.ElementRef<typeof DialogPrimitive.Content>,
|
|
32
|
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
|
33
|
+
>(({ className, children, ...props }, ref) => (
|
|
34
|
+
<DialogPortal>
|
|
35
|
+
<DialogOverlay />
|
|
36
|
+
<DialogPrimitive.Content
|
|
37
|
+
ref={ref}
|
|
38
|
+
className={cn(
|
|
39
|
+
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border border-[hsl(40,20%,90%)] bg-white p-6 shadow-2xl duration-200 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-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] rounded-2xl",
|
|
40
|
+
className
|
|
41
|
+
)}
|
|
42
|
+
{...props}
|
|
43
|
+
>
|
|
44
|
+
{children}
|
|
45
|
+
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-lg opacity-70 ring-offset-white transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-[hsl(25,95%,53%)] focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-[hsl(40,20%,96%)] data-[state=open]:text-[hsl(30,8%,45%)]">
|
|
46
|
+
<X className="h-4 w-4 text-[hsl(30,8%,45%)]" />
|
|
47
|
+
<span className="sr-only">Close</span>
|
|
48
|
+
</DialogPrimitive.Close>
|
|
49
|
+
</DialogPrimitive.Content>
|
|
50
|
+
</DialogPortal>
|
|
51
|
+
))
|
|
52
|
+
DialogContent.displayName = DialogPrimitive.Content.displayName
|
|
53
|
+
|
|
54
|
+
const DialogHeader = ({
|
|
55
|
+
className,
|
|
56
|
+
...props
|
|
57
|
+
}: React.HTMLAttributes<HTMLDivElement>) => (
|
|
58
|
+
<div
|
|
59
|
+
className={cn(
|
|
60
|
+
"flex flex-col space-y-1.5 text-center sm:text-left",
|
|
61
|
+
className
|
|
62
|
+
)}
|
|
63
|
+
{...props}
|
|
64
|
+
/>
|
|
65
|
+
)
|
|
66
|
+
DialogHeader.displayName = "DialogHeader"
|
|
67
|
+
|
|
68
|
+
const DialogFooter = ({
|
|
69
|
+
className,
|
|
70
|
+
...props
|
|
71
|
+
}: React.HTMLAttributes<HTMLDivElement>) => (
|
|
72
|
+
<div
|
|
73
|
+
className={cn(
|
|
74
|
+
"flex flex-col-reverse sm:flex-row sm:justify-end sm:gap-2",
|
|
75
|
+
className
|
|
76
|
+
)}
|
|
77
|
+
{...props}
|
|
78
|
+
/>
|
|
79
|
+
)
|
|
80
|
+
DialogFooter.displayName = "DialogFooter"
|
|
81
|
+
|
|
82
|
+
const DialogTitle = React.forwardRef<
|
|
83
|
+
React.ElementRef<typeof DialogPrimitive.Title>,
|
|
84
|
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
|
85
|
+
>(({ className, ...props }, ref) => (
|
|
86
|
+
<DialogPrimitive.Title
|
|
87
|
+
ref={ref}
|
|
88
|
+
className={cn(
|
|
89
|
+
"text-lg font-semibold leading-none tracking-tight text-[hsl(30,20%,12%)]",
|
|
90
|
+
className
|
|
91
|
+
)}
|
|
92
|
+
{...props}
|
|
93
|
+
/>
|
|
94
|
+
))
|
|
95
|
+
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
|
96
|
+
|
|
97
|
+
const DialogDescription = React.forwardRef<
|
|
98
|
+
React.ElementRef<typeof DialogPrimitive.Description>,
|
|
99
|
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
|
100
|
+
>(({ className, ...props }, ref) => (
|
|
101
|
+
<DialogPrimitive.Description
|
|
102
|
+
ref={ref}
|
|
103
|
+
className={cn("text-sm text-[hsl(30,8%,45%)]", className)}
|
|
104
|
+
{...props}
|
|
105
|
+
/>
|
|
106
|
+
))
|
|
107
|
+
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
|
108
|
+
|
|
109
|
+
export {
|
|
110
|
+
Dialog,
|
|
111
|
+
DialogPortal,
|
|
112
|
+
DialogOverlay,
|
|
113
|
+
DialogClose,
|
|
114
|
+
DialogTrigger,
|
|
115
|
+
DialogContent,
|
|
116
|
+
DialogHeader,
|
|
117
|
+
DialogFooter,
|
|
118
|
+
DialogTitle,
|
|
119
|
+
DialogDescription,
|
|
120
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { cn } from '@/lib/utils';
|
|
3
|
+
|
|
4
|
+
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
|
|
5
|
+
|
|
6
|
+
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
7
|
+
({ className, type, ...props }, ref) => {
|
|
8
|
+
return (
|
|
9
|
+
<input
|
|
10
|
+
type={type}
|
|
11
|
+
className={cn(
|
|
12
|
+
'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
|
13
|
+
className
|
|
14
|
+
)}
|
|
15
|
+
ref={ref}
|
|
16
|
+
{...props}
|
|
17
|
+
/>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
);
|
|
21
|
+
Input.displayName = 'Input';
|
|
22
|
+
|
|
23
|
+
export { Input };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { cn } from '@/lib/utils';
|
|
3
|
+
|
|
4
|
+
export interface LabelProps extends React.LabelHTMLAttributes<HTMLLabelElement> {}
|
|
5
|
+
|
|
6
|
+
const Label = React.forwardRef<HTMLLabelElement, LabelProps>(
|
|
7
|
+
({ className, ...props }, ref) => (
|
|
8
|
+
<label
|
|
9
|
+
ref={ref}
|
|
10
|
+
className={cn(
|
|
11
|
+
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
|
|
12
|
+
className
|
|
13
|
+
)}
|
|
14
|
+
{...props}
|
|
15
|
+
/>
|
|
16
|
+
)
|
|
17
|
+
);
|
|
18
|
+
Label.displayName = 'Label';
|
|
19
|
+
|
|
20
|
+
export { Label };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { cn } from '@/lib/utils';
|
|
3
|
+
|
|
4
|
+
interface ScrollAreaProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
5
|
+
children: React.ReactNode;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const ScrollArea = React.forwardRef<HTMLDivElement, ScrollAreaProps>(
|
|
9
|
+
({ className, children, ...props }, ref) => (
|
|
10
|
+
<div
|
|
11
|
+
ref={ref}
|
|
12
|
+
className={cn('overflow-auto', className)}
|
|
13
|
+
{...props}
|
|
14
|
+
>
|
|
15
|
+
{children}
|
|
16
|
+
</div>
|
|
17
|
+
)
|
|
18
|
+
);
|
|
19
|
+
ScrollArea.displayName = 'ScrollArea';
|
|
20
|
+
|
|
21
|
+
export { ScrollArea };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { cn } from "@/lib/utils";
|
|
2
|
+
|
|
3
|
+
function Skeleton({
|
|
4
|
+
className,
|
|
5
|
+
...props
|
|
6
|
+
}: React.HTMLAttributes<HTMLDivElement>) {
|
|
7
|
+
return (
|
|
8
|
+
<div
|
|
9
|
+
className={cn("animate-pulse rounded-md bg-slate-200", className)}
|
|
10
|
+
{...props}
|
|
11
|
+
/>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export { Skeleton };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { cn } from '@/lib/utils';
|
|
3
|
+
|
|
4
|
+
interface SwitchProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'onChange'> {
|
|
5
|
+
checked?: boolean;
|
|
6
|
+
onCheckedChange?: (checked: boolean) => void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const Switch = React.forwardRef<HTMLButtonElement, SwitchProps>(
|
|
10
|
+
({ className, checked = false, onCheckedChange, ...props }, ref) => {
|
|
11
|
+
return (
|
|
12
|
+
<button
|
|
13
|
+
type="button"
|
|
14
|
+
role="switch"
|
|
15
|
+
aria-checked={checked}
|
|
16
|
+
ref={ref}
|
|
17
|
+
className={cn(
|
|
18
|
+
'peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50',
|
|
19
|
+
checked ? 'bg-primary' : 'bg-input',
|
|
20
|
+
className
|
|
21
|
+
)}
|
|
22
|
+
onClick={() => onCheckedChange?.(!checked)}
|
|
23
|
+
{...props}
|
|
24
|
+
>
|
|
25
|
+
<span
|
|
26
|
+
className={cn(
|
|
27
|
+
'pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform',
|
|
28
|
+
checked ? 'translate-x-5' : 'translate-x-0'
|
|
29
|
+
)}
|
|
30
|
+
/>
|
|
31
|
+
</button>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
);
|
|
35
|
+
Switch.displayName = 'Switch';
|
|
36
|
+
|
|
37
|
+
export { Switch };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { cn } from '@/lib/utils';
|
|
3
|
+
|
|
4
|
+
interface Tab {
|
|
5
|
+
id: string;
|
|
6
|
+
label: string;
|
|
7
|
+
count?: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface TabsProps {
|
|
11
|
+
tabs: Tab[];
|
|
12
|
+
activeTab: string;
|
|
13
|
+
onChange: (id: string) => void;
|
|
14
|
+
className?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function Tabs({ tabs, activeTab, onChange, className }: TabsProps) {
|
|
18
|
+
return (
|
|
19
|
+
<div className={cn('flex items-center gap-8 border-b border-[hsl(40,10%,94%)] mb-8', className)}>
|
|
20
|
+
{tabs.map((tab) => {
|
|
21
|
+
const isActive = activeTab === tab.id;
|
|
22
|
+
return (
|
|
23
|
+
<button
|
|
24
|
+
key={tab.id}
|
|
25
|
+
onClick={() => onChange(tab.id)}
|
|
26
|
+
className={cn(
|
|
27
|
+
'relative pb-4 text-[15px] font-semibold transition-all duration-200 flex items-center gap-2',
|
|
28
|
+
isActive
|
|
29
|
+
? 'text-[hsl(30,15%,10%)]'
|
|
30
|
+
: 'text-[hsl(30,8%,55%)] hover:text-[hsl(30,15%,10%)]'
|
|
31
|
+
)}
|
|
32
|
+
>
|
|
33
|
+
{tab.label}
|
|
34
|
+
{tab.count !== undefined && (
|
|
35
|
+
<span className="text-[11px] font-medium text-[hsl(30,8%,65%)]">{tab.count.toLocaleString()}</span>
|
|
36
|
+
)}
|
|
37
|
+
{isActive && (
|
|
38
|
+
<div className="absolute bottom-0 left-0 right-0 h-0.5 bg-[hsl(30,15%,10%)] animate-in fade-in slide-in-from-left-2 duration-300" />
|
|
39
|
+
)}
|
|
40
|
+
</button>
|
|
41
|
+
);
|
|
42
|
+
})}
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { cn } from '@/lib/utils';
|
|
3
|
+
|
|
4
|
+
interface TabsProps {
|
|
5
|
+
defaultValue?: string;
|
|
6
|
+
value: string;
|
|
7
|
+
onValueChange: (value: string) => void;
|
|
8
|
+
children: React.ReactNode;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface TabsListProps {
|
|
12
|
+
children: React.ReactNode;
|
|
13
|
+
className?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface TabsTriggerProps {
|
|
17
|
+
value: string;
|
|
18
|
+
children: React.ReactNode;
|
|
19
|
+
className?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface TabsContentProps {
|
|
23
|
+
value: string;
|
|
24
|
+
children: React.ReactNode;
|
|
25
|
+
className?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const TabsContext = React.createContext<{
|
|
29
|
+
value: string;
|
|
30
|
+
onValueChange: (value: string) => void;
|
|
31
|
+
} | null>(null);
|
|
32
|
+
|
|
33
|
+
export function Tabs({ defaultValue: _defaultValue, value, onValueChange, children }: TabsProps) {
|
|
34
|
+
return (
|
|
35
|
+
<TabsContext.Provider value={{ value, onValueChange }}>
|
|
36
|
+
{children}
|
|
37
|
+
</TabsContext.Provider>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function TabsList({ children, className }: TabsListProps) {
|
|
42
|
+
return (
|
|
43
|
+
<div
|
|
44
|
+
className={cn(
|
|
45
|
+
'inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground',
|
|
46
|
+
className
|
|
47
|
+
)}
|
|
48
|
+
>
|
|
49
|
+
{children}
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function TabsTrigger({ value, children, className }: TabsTriggerProps) {
|
|
55
|
+
const context = React.useContext(TabsContext);
|
|
56
|
+
if (!context) throw new Error('TabsTrigger must be used within Tabs');
|
|
57
|
+
|
|
58
|
+
const isActive = context.value === value;
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<button
|
|
62
|
+
type="button"
|
|
63
|
+
onClick={() => context.onValueChange(value)}
|
|
64
|
+
className={cn(
|
|
65
|
+
'inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
|
|
66
|
+
isActive
|
|
67
|
+
? 'bg-background text-foreground shadow-sm'
|
|
68
|
+
: 'hover:bg-background/50',
|
|
69
|
+
className
|
|
70
|
+
)}
|
|
71
|
+
>
|
|
72
|
+
{children}
|
|
73
|
+
</button>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function TabsContent({ value, children, className }: TabsContentProps) {
|
|
78
|
+
const context = React.useContext(TabsContext);
|
|
79
|
+
if (!context) throw new Error('TabsContent must be used within Tabs');
|
|
80
|
+
|
|
81
|
+
if (context.value !== value) return null;
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<div className={cn('mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2', className)}>
|
|
85
|
+
{children}
|
|
86
|
+
</div>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
2
|
+
import { fetchConfig, fetchConfigMeta, updateModel, updateProvider, updateChannel, updateUiConfig, reloadConfig } from '@/api/config';
|
|
3
|
+
import { toast } from 'sonner';
|
|
4
|
+
import { t } from '@/lib/i18n';
|
|
5
|
+
|
|
6
|
+
export function useConfig() {
|
|
7
|
+
return useQuery({
|
|
8
|
+
queryKey: ['config'],
|
|
9
|
+
queryFn: fetchConfig,
|
|
10
|
+
staleTime: 30_000,
|
|
11
|
+
refetchOnWindowFocus: true
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function useConfigMeta() {
|
|
16
|
+
return useQuery({
|
|
17
|
+
queryKey: ['config-meta'],
|
|
18
|
+
queryFn: fetchConfigMeta,
|
|
19
|
+
staleTime: Infinity
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function useUpdateModel() {
|
|
24
|
+
const queryClient = useQueryClient();
|
|
25
|
+
|
|
26
|
+
return useMutation({
|
|
27
|
+
mutationFn: updateModel,
|
|
28
|
+
onSuccess: () => {
|
|
29
|
+
queryClient.invalidateQueries({ queryKey: ['config'] });
|
|
30
|
+
toast.success(t('configSaved'));
|
|
31
|
+
},
|
|
32
|
+
onError: (error: Error) => {
|
|
33
|
+
toast.error(t('configSaveFailed') + ': ' + error.message);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function useUpdateProvider() {
|
|
39
|
+
const queryClient = useQueryClient();
|
|
40
|
+
|
|
41
|
+
return useMutation({
|
|
42
|
+
mutationFn: ({ provider, data }: { provider: string; data: unknown }) =>
|
|
43
|
+
updateProvider(provider, data as Parameters<typeof updateProvider>[1]),
|
|
44
|
+
onSuccess: () => {
|
|
45
|
+
queryClient.invalidateQueries({ queryKey: ['config'] });
|
|
46
|
+
toast.success(t('configSaved'));
|
|
47
|
+
},
|
|
48
|
+
onError: (error: Error) => {
|
|
49
|
+
toast.error(t('configSaveFailed') + ': ' + error.message);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function useUpdateChannel() {
|
|
55
|
+
const queryClient = useQueryClient();
|
|
56
|
+
|
|
57
|
+
return useMutation({
|
|
58
|
+
mutationFn: ({ channel, data }: { channel: string; data: unknown }) =>
|
|
59
|
+
updateChannel(channel, data as Parameters<typeof updateChannel>[1]),
|
|
60
|
+
onSuccess: () => {
|
|
61
|
+
queryClient.invalidateQueries({ queryKey: ['config'] });
|
|
62
|
+
toast.success(t('configSaved'));
|
|
63
|
+
},
|
|
64
|
+
onError: (error: Error) => {
|
|
65
|
+
toast.error(t('configSaveFailed') + ': ' + error.message);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function useUpdateUiConfig() {
|
|
71
|
+
const queryClient = useQueryClient();
|
|
72
|
+
|
|
73
|
+
return useMutation({
|
|
74
|
+
mutationFn: updateUiConfig,
|
|
75
|
+
onSuccess: () => {
|
|
76
|
+
queryClient.invalidateQueries({ queryKey: ['config'] });
|
|
77
|
+
toast.success(t('configSaved'));
|
|
78
|
+
},
|
|
79
|
+
onError: (error: Error) => {
|
|
80
|
+
toast.error(t('configSaveFailed') + ': ' + error.message);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function useReloadConfig() {
|
|
86
|
+
return useMutation({
|
|
87
|
+
mutationFn: reloadConfig,
|
|
88
|
+
onSuccess: () => {
|
|
89
|
+
toast.success(t('configReloaded'));
|
|
90
|
+
},
|
|
91
|
+
onError: (error: Error) => {
|
|
92
|
+
toast.error(t('configReloadFailed') + ': ' + error.message);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { ConfigWebSocket } from '@/api/websocket';
|
|
3
|
+
import { useUiStore } from '@/stores/ui.store';
|
|
4
|
+
import type { QueryClient } from '@tanstack/react-query';
|
|
5
|
+
|
|
6
|
+
export function useWebSocket(queryClient?: QueryClient) {
|
|
7
|
+
const [ws, setWs] = useState<ConfigWebSocket | null>(null);
|
|
8
|
+
const { setConnectionStatus } = useUiStore();
|
|
9
|
+
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
const wsUrl = `ws://127.0.0.1:18791/ws`;
|
|
12
|
+
const client = new ConfigWebSocket(wsUrl);
|
|
13
|
+
|
|
14
|
+
client.on('connection.open', () => {
|
|
15
|
+
setConnectionStatus('connected');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
client.on('config.updated', () => {
|
|
19
|
+
// Trigger refetch of config
|
|
20
|
+
if (queryClient) {
|
|
21
|
+
queryClient.invalidateQueries({ queryKey: ['config'] });
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
client.on('error', (event) => {
|
|
26
|
+
if (event.type === 'error') {
|
|
27
|
+
console.error('WebSocket error:', event.payload.message);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
client.connect();
|
|
32
|
+
setWs(client);
|
|
33
|
+
|
|
34
|
+
return () => client.disconnect();
|
|
35
|
+
}, [setConnectionStatus, queryClient]);
|
|
36
|
+
|
|
37
|
+
return ws;
|
|
38
|
+
}
|