fluxy-bot 0.1.0
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/bin/cli.js +469 -0
- package/client/index.html +13 -0
- package/client/public/fluxy.png +0 -0
- package/client/public/icons/claude.png +0 -0
- package/client/public/icons/codex.png +0 -0
- package/client/public/icons/openai.svg +15 -0
- package/client/src/App.tsx +81 -0
- package/client/src/components/Chat/ChatView.tsx +19 -0
- package/client/src/components/Chat/InputBar.tsx +242 -0
- package/client/src/components/Chat/MessageBubble.tsx +20 -0
- package/client/src/components/Chat/MessageList.tsx +39 -0
- package/client/src/components/Chat/TypingIndicator.tsx +10 -0
- package/client/src/components/Dashboard/ConversationAnalytics.tsx +84 -0
- package/client/src/components/Dashboard/DashboardPage.tsx +52 -0
- package/client/src/components/Dashboard/PromoCard.tsx +44 -0
- package/client/src/components/Dashboard/ReportCard.tsx +35 -0
- package/client/src/components/Dashboard/TodayStats.tsx +28 -0
- package/client/src/components/ErrorBoundary.tsx +23 -0
- package/client/src/components/FluxyFab.tsx +25 -0
- package/client/src/components/Layout/ConnectionStatus.tsx +8 -0
- package/client/src/components/Layout/DashboardHeader.tsx +90 -0
- package/client/src/components/Layout/DashboardLayout.tsx +24 -0
- package/client/src/components/Layout/Header.tsx +10 -0
- package/client/src/components/Layout/MobileNav.tsx +30 -0
- package/client/src/components/Layout/Sidebar.tsx +55 -0
- package/client/src/components/Onboard/OnboardWizard.tsx +763 -0
- package/client/src/components/ui/avatar.tsx +109 -0
- package/client/src/components/ui/badge.tsx +48 -0
- package/client/src/components/ui/button.tsx +64 -0
- package/client/src/components/ui/card.tsx +92 -0
- package/client/src/components/ui/dialog.tsx +156 -0
- package/client/src/components/ui/dropdown-menu.tsx +257 -0
- package/client/src/components/ui/input.tsx +21 -0
- package/client/src/components/ui/scroll-area.tsx +58 -0
- package/client/src/components/ui/select.tsx +190 -0
- package/client/src/components/ui/separator.tsx +28 -0
- package/client/src/components/ui/sheet.tsx +141 -0
- package/client/src/components/ui/skeleton.tsx +13 -0
- package/client/src/components/ui/switch.tsx +33 -0
- package/client/src/components/ui/tabs.tsx +89 -0
- package/client/src/components/ui/textarea.tsx +18 -0
- package/client/src/components/ui/tooltip.tsx +55 -0
- package/client/src/hooks/useChat.ts +69 -0
- package/client/src/hooks/useMobile.ts +16 -0
- package/client/src/hooks/useWebSocket.ts +24 -0
- package/client/src/lib/mock-data.ts +104 -0
- package/client/src/lib/utils.ts +6 -0
- package/client/src/lib/ws-client.ts +52 -0
- package/client/src/main.tsx +10 -0
- package/client/src/styles/globals.css +55 -0
- package/components.json +20 -0
- package/dist/assets/index-BkNWpS06.css +1 -0
- package/dist/assets/index-CX3QeqQ8.js +64 -0
- package/dist/fluxy.png +0 -0
- package/dist/icons/claude.png +0 -0
- package/dist/icons/codex.png +0 -0
- package/dist/icons/openai.svg +15 -0
- package/dist/index.html +14 -0
- package/dist/manifest.webmanifest +1 -0
- package/dist/registerSW.js +1 -0
- package/dist/sw.js +1 -0
- package/dist/workbox-8c29f6e4.js +1 -0
- package/package.json +82 -0
- package/postcss.config.js +5 -0
- package/shared/ai.ts +141 -0
- package/shared/config.ts +37 -0
- package/shared/logger.ts +13 -0
- package/shared/paths.ts +14 -0
- package/shared/relay.ts +101 -0
- package/supervisor/fluxy.html +94 -0
- package/supervisor/index.ts +173 -0
- package/supervisor/tunnel.ts +62 -0
- package/supervisor/worker.ts +55 -0
- package/tsconfig.json +20 -0
- package/vite.config.ts +38 -0
- package/worker/claude-auth.ts +224 -0
- package/worker/codex-auth.ts +199 -0
- package/worker/db.ts +75 -0
- package/worker/index.ts +169 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { cn } from "@/lib/utils"
|
|
2
|
+
|
|
3
|
+
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
|
|
4
|
+
return (
|
|
5
|
+
<div
|
|
6
|
+
data-slot="skeleton"
|
|
7
|
+
className={cn("bg-accent animate-pulse rounded-md", className)}
|
|
8
|
+
{...props}
|
|
9
|
+
/>
|
|
10
|
+
)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export { Skeleton }
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { Switch as SwitchPrimitive } from "radix-ui"
|
|
3
|
+
|
|
4
|
+
import { cn } from "@/lib/utils"
|
|
5
|
+
|
|
6
|
+
function Switch({
|
|
7
|
+
className,
|
|
8
|
+
size = "default",
|
|
9
|
+
...props
|
|
10
|
+
}: React.ComponentProps<typeof SwitchPrimitive.Root> & {
|
|
11
|
+
size?: "sm" | "default"
|
|
12
|
+
}) {
|
|
13
|
+
return (
|
|
14
|
+
<SwitchPrimitive.Root
|
|
15
|
+
data-slot="switch"
|
|
16
|
+
data-size={size}
|
|
17
|
+
className={cn(
|
|
18
|
+
"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 group/switch inline-flex 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 data-[size=default]:h-[1.15rem] data-[size=default]:w-8 data-[size=sm]:h-3.5 data-[size=sm]:w-6",
|
|
19
|
+
className
|
|
20
|
+
)}
|
|
21
|
+
{...props}
|
|
22
|
+
>
|
|
23
|
+
<SwitchPrimitive.Thumb
|
|
24
|
+
data-slot="switch-thumb"
|
|
25
|
+
className={cn(
|
|
26
|
+
"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block rounded-full ring-0 transition-transform group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0"
|
|
27
|
+
)}
|
|
28
|
+
/>
|
|
29
|
+
</SwitchPrimitive.Root>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export { Switch }
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
3
|
+
import { Tabs as TabsPrimitive } from "radix-ui"
|
|
4
|
+
|
|
5
|
+
import { cn } from "@/lib/utils"
|
|
6
|
+
|
|
7
|
+
function Tabs({
|
|
8
|
+
className,
|
|
9
|
+
orientation = "horizontal",
|
|
10
|
+
...props
|
|
11
|
+
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
|
|
12
|
+
return (
|
|
13
|
+
<TabsPrimitive.Root
|
|
14
|
+
data-slot="tabs"
|
|
15
|
+
data-orientation={orientation}
|
|
16
|
+
orientation={orientation}
|
|
17
|
+
className={cn(
|
|
18
|
+
"group/tabs flex gap-2 data-[orientation=horizontal]:flex-col",
|
|
19
|
+
className
|
|
20
|
+
)}
|
|
21
|
+
{...props}
|
|
22
|
+
/>
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const tabsListVariants = cva(
|
|
27
|
+
"rounded-lg p-[3px] group-data-[orientation=horizontal]/tabs:h-9 data-[variant=line]:rounded-none group/tabs-list text-muted-foreground inline-flex w-fit items-center justify-center group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col",
|
|
28
|
+
{
|
|
29
|
+
variants: {
|
|
30
|
+
variant: {
|
|
31
|
+
default: "bg-muted",
|
|
32
|
+
line: "gap-1 bg-transparent",
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
defaultVariants: {
|
|
36
|
+
variant: "default",
|
|
37
|
+
},
|
|
38
|
+
}
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
function TabsList({
|
|
42
|
+
className,
|
|
43
|
+
variant = "default",
|
|
44
|
+
...props
|
|
45
|
+
}: React.ComponentProps<typeof TabsPrimitive.List> &
|
|
46
|
+
VariantProps<typeof tabsListVariants>) {
|
|
47
|
+
return (
|
|
48
|
+
<TabsPrimitive.List
|
|
49
|
+
data-slot="tabs-list"
|
|
50
|
+
data-variant={variant}
|
|
51
|
+
className={cn(tabsListVariants({ variant }), className)}
|
|
52
|
+
{...props}
|
|
53
|
+
/>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function TabsTrigger({
|
|
58
|
+
className,
|
|
59
|
+
...props
|
|
60
|
+
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
|
|
61
|
+
return (
|
|
62
|
+
<TabsPrimitive.Trigger
|
|
63
|
+
data-slot="tabs-trigger"
|
|
64
|
+
className={cn(
|
|
65
|
+
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring text-foreground/60 hover:text-foreground dark:text-muted-foreground dark: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-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start focus-visible:ring-[3px] focus-visible:outline-1 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",
|
|
66
|
+
"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",
|
|
67
|
+
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 data-[state=active]:text-foreground",
|
|
68
|
+
"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",
|
|
69
|
+
className
|
|
70
|
+
)}
|
|
71
|
+
{...props}
|
|
72
|
+
/>
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function TabsContent({
|
|
77
|
+
className,
|
|
78
|
+
...props
|
|
79
|
+
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
|
|
80
|
+
return (
|
|
81
|
+
<TabsPrimitive.Content
|
|
82
|
+
data-slot="tabs-content"
|
|
83
|
+
className={cn("flex-1 outline-none", className)}
|
|
84
|
+
{...props}
|
|
85
|
+
/>
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants }
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
|
|
3
|
+
import { cn } from "@/lib/utils"
|
|
4
|
+
|
|
5
|
+
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
|
6
|
+
return (
|
|
7
|
+
<textarea
|
|
8
|
+
data-slot="textarea"
|
|
9
|
+
className={cn(
|
|
10
|
+
"border-input placeholder: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 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
|
11
|
+
className
|
|
12
|
+
)}
|
|
13
|
+
{...props}
|
|
14
|
+
/>
|
|
15
|
+
)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export { Textarea }
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { Tooltip as TooltipPrimitive } from "radix-ui"
|
|
3
|
+
|
|
4
|
+
import { cn } from "@/lib/utils"
|
|
5
|
+
|
|
6
|
+
function TooltipProvider({
|
|
7
|
+
delayDuration = 0,
|
|
8
|
+
...props
|
|
9
|
+
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
|
|
10
|
+
return (
|
|
11
|
+
<TooltipPrimitive.Provider
|
|
12
|
+
data-slot="tooltip-provider"
|
|
13
|
+
delayDuration={delayDuration}
|
|
14
|
+
{...props}
|
|
15
|
+
/>
|
|
16
|
+
)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function Tooltip({
|
|
20
|
+
...props
|
|
21
|
+
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
|
|
22
|
+
return <TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function TooltipTrigger({
|
|
26
|
+
...props
|
|
27
|
+
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
|
|
28
|
+
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function TooltipContent({
|
|
32
|
+
className,
|
|
33
|
+
sideOffset = 0,
|
|
34
|
+
children,
|
|
35
|
+
...props
|
|
36
|
+
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
|
|
37
|
+
return (
|
|
38
|
+
<TooltipPrimitive.Portal>
|
|
39
|
+
<TooltipPrimitive.Content
|
|
40
|
+
data-slot="tooltip-content"
|
|
41
|
+
sideOffset={sideOffset}
|
|
42
|
+
className={cn(
|
|
43
|
+
"bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-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 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
|
|
44
|
+
className
|
|
45
|
+
)}
|
|
46
|
+
{...props}
|
|
47
|
+
>
|
|
48
|
+
{children}
|
|
49
|
+
<TooltipPrimitive.Arrow className="bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
|
|
50
|
+
</TooltipPrimitive.Content>
|
|
51
|
+
</TooltipPrimitive.Portal>
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
2
|
+
import type { WsClient } from '../lib/ws-client';
|
|
3
|
+
|
|
4
|
+
export interface ChatMessage {
|
|
5
|
+
id: string;
|
|
6
|
+
role: 'user' | 'assistant';
|
|
7
|
+
content: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function useChat(ws: WsClient | null) {
|
|
11
|
+
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
|
12
|
+
const [conversationId, setConversationId] = useState<string | null>(null);
|
|
13
|
+
const [streaming, setStreaming] = useState(false);
|
|
14
|
+
const [streamBuffer, setStreamBuffer] = useState('');
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
if (!ws) return;
|
|
18
|
+
|
|
19
|
+
const unsubs = [
|
|
20
|
+
ws.on('bot:typing', () => setStreaming(true)),
|
|
21
|
+
ws.on('bot:token', (data: { token: string }) => {
|
|
22
|
+
setStreamBuffer((buf) => buf + data.token);
|
|
23
|
+
}),
|
|
24
|
+
ws.on('bot:response', (data: { conversationId: string; messageId: string; content: string }) => {
|
|
25
|
+
setConversationId(data.conversationId);
|
|
26
|
+
setMessages((msgs) => [...msgs, { id: data.messageId, role: 'assistant', content: data.content }]);
|
|
27
|
+
setStreamBuffer('');
|
|
28
|
+
setStreaming(false);
|
|
29
|
+
}),
|
|
30
|
+
ws.on('bot:error', (data: { error: string }) => {
|
|
31
|
+
setStreamBuffer('');
|
|
32
|
+
setStreaming(false);
|
|
33
|
+
setMessages((msgs) => [
|
|
34
|
+
...msgs,
|
|
35
|
+
{ id: Date.now().toString(), role: 'assistant', content: `Error: ${data.error}` },
|
|
36
|
+
]);
|
|
37
|
+
}),
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
return () => unsubs.forEach((u) => u());
|
|
41
|
+
}, [ws]);
|
|
42
|
+
|
|
43
|
+
const sendMessage = useCallback(
|
|
44
|
+
(content: string) => {
|
|
45
|
+
if (!ws || !content.trim()) return;
|
|
46
|
+
|
|
47
|
+
const userMsg: ChatMessage = { id: Date.now().toString(), role: 'user', content };
|
|
48
|
+
setMessages((msgs) => [...msgs, userMsg]);
|
|
49
|
+
ws.send('user:message', { conversationId, content });
|
|
50
|
+
},
|
|
51
|
+
[ws, conversationId],
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const stopStreaming = useCallback(() => {
|
|
55
|
+
if (!ws || !conversationId) return;
|
|
56
|
+
ws.send('user:stop', { conversationId });
|
|
57
|
+
setStreaming(false);
|
|
58
|
+
setStreamBuffer('');
|
|
59
|
+
}, [ws, conversationId]);
|
|
60
|
+
|
|
61
|
+
const newChat = useCallback(() => {
|
|
62
|
+
setMessages([]);
|
|
63
|
+
setConversationId(null);
|
|
64
|
+
setStreamBuffer('');
|
|
65
|
+
setStreaming(false);
|
|
66
|
+
}, []);
|
|
67
|
+
|
|
68
|
+
return { messages, streaming, streamBuffer, conversationId, sendMessage, stopStreaming, newChat };
|
|
69
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
export function useMobile(breakpoint = 768) {
|
|
4
|
+
const [isMobile, setIsMobile] = useState(false);
|
|
5
|
+
|
|
6
|
+
useEffect(() => {
|
|
7
|
+
const mql = window.matchMedia(`(max-width: ${breakpoint - 1}px)`);
|
|
8
|
+
const onChange = (e: MediaQueryListEvent | MediaQueryList) =>
|
|
9
|
+
setIsMobile(e.matches);
|
|
10
|
+
onChange(mql);
|
|
11
|
+
mql.addEventListener('change', onChange);
|
|
12
|
+
return () => mql.removeEventListener('change', onChange);
|
|
13
|
+
}, [breakpoint]);
|
|
14
|
+
|
|
15
|
+
return isMobile;
|
|
16
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { WsClient } from '../lib/ws-client';
|
|
3
|
+
|
|
4
|
+
export function useWebSocket() {
|
|
5
|
+
const clientRef = useRef<WsClient | null>(null);
|
|
6
|
+
const [connected, setConnected] = useState(false);
|
|
7
|
+
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
const client = new WsClient();
|
|
10
|
+
clientRef.current = client;
|
|
11
|
+
client.connect();
|
|
12
|
+
|
|
13
|
+
const checkConnection = setInterval(() => {
|
|
14
|
+
setConnected(client.connected);
|
|
15
|
+
}, 1000);
|
|
16
|
+
|
|
17
|
+
return () => {
|
|
18
|
+
clearInterval(checkConnection);
|
|
19
|
+
client.disconnect();
|
|
20
|
+
};
|
|
21
|
+
}, []);
|
|
22
|
+
|
|
23
|
+
return { ws: clientRef.current, connected };
|
|
24
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import {
|
|
2
|
+
LayoutDashboard,
|
|
3
|
+
MessageSquare,
|
|
4
|
+
Brain,
|
|
5
|
+
BookOpen,
|
|
6
|
+
ScrollText,
|
|
7
|
+
Settings,
|
|
8
|
+
type LucideIcon,
|
|
9
|
+
} from 'lucide-react';
|
|
10
|
+
|
|
11
|
+
/* ── Navigation ─────────────────────────────── */
|
|
12
|
+
|
|
13
|
+
export interface NavItem {
|
|
14
|
+
label: string;
|
|
15
|
+
icon: LucideIcon;
|
|
16
|
+
href: string;
|
|
17
|
+
active?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface NavSection {
|
|
21
|
+
label: string;
|
|
22
|
+
items: NavItem[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const navSections: NavSection[] = [
|
|
26
|
+
{
|
|
27
|
+
label: 'HOME',
|
|
28
|
+
items: [
|
|
29
|
+
{ label: 'Overview', icon: LayoutDashboard, href: '#', active: true },
|
|
30
|
+
{ label: 'Conversations', icon: MessageSquare, href: '#' },
|
|
31
|
+
{ label: 'Models', icon: Brain, href: '#' },
|
|
32
|
+
{ label: 'Knowledge', icon: BookOpen, href: '#' },
|
|
33
|
+
],
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
label: 'SYSTEM',
|
|
37
|
+
items: [
|
|
38
|
+
{ label: 'Logs', icon: ScrollText, href: '#' },
|
|
39
|
+
{ label: 'Settings', icon: Settings, href: '#' },
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
/* ── Weekly activity grid ───────────────────── */
|
|
45
|
+
|
|
46
|
+
export interface WeeklyModelActivity {
|
|
47
|
+
model: string;
|
|
48
|
+
/** 0–4 intensity per day (Mon–Sun) */
|
|
49
|
+
days: number[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const weeklyActivity: WeeklyModelActivity[] = [
|
|
53
|
+
{ model: 'GPT-4o', days: [3, 2, 4, 3, 1, 2, 0] },
|
|
54
|
+
{ model: 'Claude', days: [2, 4, 3, 4, 2, 1, 1] },
|
|
55
|
+
{ model: 'Local', days: [1, 1, 2, 1, 3, 4, 2] },
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
export const dayLabels = ['M', 'T', 'W', 'T', 'F', 'S', 'S'];
|
|
59
|
+
|
|
60
|
+
export const activityColors = [
|
|
61
|
+
'#2a2a2a', // 0 – none
|
|
62
|
+
'#1e3a5f', // 1 – low
|
|
63
|
+
'#2c5a8f', // 2 – medium-low
|
|
64
|
+
'#3578bf', // 3 – medium-high
|
|
65
|
+
'#3C8FFF', // 4 – high
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
export const activityLegend = [
|
|
69
|
+
{ label: '< 10', level: 1 },
|
|
70
|
+
{ label: '10–50', level: 2 },
|
|
71
|
+
{ label: '> 50', level: 3 },
|
|
72
|
+
{ label: '100+', level: 4 },
|
|
73
|
+
{ label: 'None', level: 0 },
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
/* ── Stats ──────────────────────────────────── */
|
|
77
|
+
|
|
78
|
+
export interface StatItem {
|
|
79
|
+
label: string;
|
|
80
|
+
value: string;
|
|
81
|
+
change: string;
|
|
82
|
+
trend: 'up' | 'down';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export const stats: StatItem[] = [
|
|
86
|
+
{ label: 'Conversations', value: '1,284', change: '+12%', trend: 'up' },
|
|
87
|
+
{ label: 'Messages Today', value: '342', change: '+8%', trend: 'up' },
|
|
88
|
+
{ label: 'Avg Response', value: '1.2s', change: '-15%', trend: 'down' },
|
|
89
|
+
{ label: 'Uptime', value: '99.9%', change: '+0.1%', trend: 'up' },
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
/* ── System status ──────────────────────────── */
|
|
93
|
+
|
|
94
|
+
export interface SystemService {
|
|
95
|
+
name: string;
|
|
96
|
+
status: 'online' | 'offline' | 'degraded';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export const systemStatus: SystemService[] = [
|
|
100
|
+
{ name: 'Worker', status: 'online' },
|
|
101
|
+
{ name: 'Tunnel', status: 'online' },
|
|
102
|
+
{ name: 'Database', status: 'online' },
|
|
103
|
+
{ name: 'AI Provider', status: 'online' },
|
|
104
|
+
];
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
type MessageHandler = (msg: any) => void;
|
|
2
|
+
|
|
3
|
+
export class WsClient {
|
|
4
|
+
private ws: WebSocket | null = null;
|
|
5
|
+
private handlers = new Map<string, Set<MessageHandler>>();
|
|
6
|
+
private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
|
7
|
+
private url: string;
|
|
8
|
+
|
|
9
|
+
constructor(url?: string) {
|
|
10
|
+
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
11
|
+
const host = import.meta.env.DEV ? 'localhost:3000' : location.host;
|
|
12
|
+
this.url = url ?? `${proto}//${host}/ws`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
connect(): void {
|
|
16
|
+
this.ws = new WebSocket(this.url);
|
|
17
|
+
|
|
18
|
+
this.ws.onmessage = (e) => {
|
|
19
|
+
const msg = JSON.parse(e.data);
|
|
20
|
+
const handlers = this.handlers.get(msg.type);
|
|
21
|
+
handlers?.forEach((h) => h(msg.data));
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
this.ws.onclose = () => {
|
|
25
|
+
this.reconnectTimer = setTimeout(() => this.connect(), 2000);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
this.ws.onerror = () => this.ws?.close();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
disconnect(): void {
|
|
32
|
+
if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
|
|
33
|
+
this.ws?.close();
|
|
34
|
+
this.ws = null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
on(type: string, handler: MessageHandler): () => void {
|
|
38
|
+
if (!this.handlers.has(type)) this.handlers.set(type, new Set());
|
|
39
|
+
this.handlers.get(type)!.add(handler);
|
|
40
|
+
return () => this.handlers.get(type)?.delete(handler);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
send(type: string, data: any): void {
|
|
44
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
45
|
+
this.ws.send(JSON.stringify({ type, data }));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
get connected(): boolean {
|
|
50
|
+
return this.ws?.readyState === WebSocket.OPEN;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
|
|
3
|
+
@custom-variant dark (&:is(.dark *));
|
|
4
|
+
|
|
5
|
+
@theme inline {
|
|
6
|
+
--color-background: #212121;
|
|
7
|
+
--color-foreground: #f5f5f5;
|
|
8
|
+
--color-card: #2a2a2a;
|
|
9
|
+
--color-card-foreground: #f5f5f5;
|
|
10
|
+
--color-popover: #2a2a2a;
|
|
11
|
+
--color-popover-foreground: #f5f5f5;
|
|
12
|
+
--color-primary: #3C8FFF;
|
|
13
|
+
--color-primary-foreground: #ffffff;
|
|
14
|
+
--color-secondary: #333333;
|
|
15
|
+
--color-secondary-foreground: #f5f5f5;
|
|
16
|
+
--color-muted: #333333;
|
|
17
|
+
--color-muted-foreground: #999999;
|
|
18
|
+
--color-accent: #333333;
|
|
19
|
+
--color-accent-foreground: #f5f5f5;
|
|
20
|
+
--color-destructive: #FD486B;
|
|
21
|
+
--color-destructive-foreground: #ffffff;
|
|
22
|
+
--color-border: #3a3a3a;
|
|
23
|
+
--color-input: #3a3a3a;
|
|
24
|
+
--color-ring: #3C8FFF;
|
|
25
|
+
--color-chart-1: #3C8FFF;
|
|
26
|
+
--color-chart-2: #FD486B;
|
|
27
|
+
--color-chart-3: #F59E0B;
|
|
28
|
+
--color-chart-4: #8B5CF6;
|
|
29
|
+
--color-chart-5: #10B981;
|
|
30
|
+
--color-sidebar: #1c1c1c;
|
|
31
|
+
--color-sidebar-foreground: #f5f5f5;
|
|
32
|
+
--color-sidebar-primary: #3C8FFF;
|
|
33
|
+
--color-sidebar-primary-foreground: #ffffff;
|
|
34
|
+
--color-sidebar-accent: #282828;
|
|
35
|
+
--color-sidebar-accent-foreground: #f5f5f5;
|
|
36
|
+
--color-sidebar-border: #3a3a3a;
|
|
37
|
+
--color-sidebar-ring: #3C8FFF;
|
|
38
|
+
--radius: 0.75rem;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
body {
|
|
42
|
+
background-color: var(--color-background);
|
|
43
|
+
color: var(--color-foreground);
|
|
44
|
+
-webkit-font-smoothing: antialiased;
|
|
45
|
+
-moz-osx-font-smoothing: grayscale;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
::selection {
|
|
49
|
+
background-color: rgba(60, 143, 255, 0.25);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
::-webkit-scrollbar { width: 6px; }
|
|
53
|
+
::-webkit-scrollbar-track { background: transparent; }
|
|
54
|
+
::-webkit-scrollbar-thumb { background: #3a3a3a; border-radius: 3px; }
|
|
55
|
+
::-webkit-scrollbar-thumb:hover { background: #4a4a4a; }
|
package/components.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema.json",
|
|
3
|
+
"style": "new-york",
|
|
4
|
+
"rsc": false,
|
|
5
|
+
"tsx": true,
|
|
6
|
+
"tailwind": {
|
|
7
|
+
"config": "",
|
|
8
|
+
"css": "client/src/styles/globals.css",
|
|
9
|
+
"baseColor": "neutral",
|
|
10
|
+
"cssVariables": true
|
|
11
|
+
},
|
|
12
|
+
"aliases": {
|
|
13
|
+
"components": "@/components",
|
|
14
|
+
"utils": "@/lib/utils",
|
|
15
|
+
"ui": "@/components/ui",
|
|
16
|
+
"lib": "@/lib",
|
|
17
|
+
"hooks": "@/hooks"
|
|
18
|
+
},
|
|
19
|
+
"iconLibrary": "lucide"
|
|
20
|
+
}
|