@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.
Files changed (48) hide show
  1. package/.eslintrc.cjs +28 -0
  2. package/CHANGELOG.md +7 -0
  3. package/dist/assets/index-BrN4G7FO.js +240 -0
  4. package/dist/assets/index-VjHB2nG6.css +1 -0
  5. package/dist/index.html +14 -0
  6. package/index.html +13 -0
  7. package/package.json +50 -0
  8. package/postcss.config.js +6 -0
  9. package/src/App.tsx +51 -0
  10. package/src/api/client.ts +40 -0
  11. package/src/api/config.ts +86 -0
  12. package/src/api/types.ts +78 -0
  13. package/src/api/websocket.ts +77 -0
  14. package/src/components/common/KeyValueEditor.tsx +65 -0
  15. package/src/components/common/MaskedInput.tsx +39 -0
  16. package/src/components/common/StatusBadge.tsx +56 -0
  17. package/src/components/common/TagInput.tsx +56 -0
  18. package/src/components/config/ChannelForm.tsx +259 -0
  19. package/src/components/config/ChannelsList.tsx +102 -0
  20. package/src/components/config/ModelConfig.tsx +181 -0
  21. package/src/components/config/ProviderForm.tsx +147 -0
  22. package/src/components/config/ProvidersList.tsx +90 -0
  23. package/src/components/config/UiConfig.tsx +189 -0
  24. package/src/components/layout/AppLayout.tsx +20 -0
  25. package/src/components/layout/Header.tsx +36 -0
  26. package/src/components/layout/Sidebar.tsx +103 -0
  27. package/src/components/ui/HighlightCard.tsx +40 -0
  28. package/src/components/ui/button.tsx +50 -0
  29. package/src/components/ui/card.tsx +78 -0
  30. package/src/components/ui/dialog.tsx +120 -0
  31. package/src/components/ui/input.tsx +23 -0
  32. package/src/components/ui/label.tsx +20 -0
  33. package/src/components/ui/scroll-area.tsx +21 -0
  34. package/src/components/ui/skeleton.tsx +15 -0
  35. package/src/components/ui/switch.tsx +37 -0
  36. package/src/components/ui/tabs-custom.tsx +45 -0
  37. package/src/components/ui/tabs.tsx +88 -0
  38. package/src/hooks/useConfig.ts +95 -0
  39. package/src/hooks/useWebSocket.ts +38 -0
  40. package/src/index.css +177 -0
  41. package/src/lib/i18n.ts +119 -0
  42. package/src/lib/utils.ts +6 -0
  43. package/src/main.tsx +10 -0
  44. package/src/stores/ui.store.ts +39 -0
  45. package/src/vite-env.d.ts +9 -0
  46. package/tailwind.config.js +43 -0
  47. package/tsconfig.json +18 -0
  48. 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
+ }