larkup 0.1.14
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/.env +2 -0
- package/app/actions/vercel.ts +417 -0
- package/app/api/chat/route.ts +192 -0
- package/app/api/chat/status/route.ts +55 -0
- package/app/api/chat/suggestions/route.ts +115 -0
- package/app/api/config/route.ts +73 -0
- package/app/api/config/test/route.ts +67 -0
- package/app/api/config/test-embedding/route.ts +40 -0
- package/app/api/config/test-llm/route.ts +47 -0
- package/app/api/config/test-provider/route.ts +36 -0
- package/app/api/demo/route.ts +176 -0
- package/app/api/deploy/ssh/route.ts +340 -0
- package/app/api/documents/route.ts +90 -0
- package/app/api/firecrawl/local/route.ts +46 -0
- package/app/api/index/route.ts +113 -0
- package/app/api/jobs/[id]/route.ts +44 -0
- package/app/api/jobs/route.ts +116 -0
- package/app/api/parse-file/route.ts +39 -0
- package/app/api/search/google/route.ts +162 -0
- package/app/api/search/route.ts +29 -0
- package/app/api/server/download/route.ts +112 -0
- package/app/api/server/generate/route.ts +46 -0
- package/app/api/server/local/route.ts +42 -0
- package/app/api/servers/route.ts +137 -0
- package/app/api/vector-stores/install/route.ts +65 -0
- package/app/api/vector-stores/status/route.ts +26 -0
- package/app/chat/page.tsx +15 -0
- package/app/configure/page.tsx +53 -0
- package/app/data/page.tsx +15 -0
- package/app/demo/page.tsx +15 -0
- package/app/favicon.ico +0 -0
- package/app/globals.css +190 -0
- package/app/index-data/page.tsx +15 -0
- package/app/layout.tsx +51 -0
- package/app/page.tsx +34 -0
- package/app/server/page.tsx +22 -0
- package/app/simple/chat/page.tsx +17 -0
- package/app/simple/docs/page.tsx +21 -0
- package/app/simple/layout.tsx +9 -0
- package/app/simple/settings/page.tsx +627 -0
- package/app/template.tsx +16 -0
- package/app/themes.css +733 -0
- package/components/alerts/generic-alert.tsx +74 -0
- package/components/app-sidebar.tsx +187 -0
- package/components/app-topnav.tsx +195 -0
- package/components/chat/chat-settings-modal.tsx +209 -0
- package/components/chat/chat-workspace.tsx +348 -0
- package/components/chat/knowledge-base-result.tsx +97 -0
- package/components/chat/message-item.tsx +103 -0
- package/components/client-layout-wrapper.tsx +182 -0
- package/components/configure/configure-form.tsx +1683 -0
- package/components/configure/store-fields.tsx +152 -0
- package/components/data/corpus-panel.tsx +499 -0
- package/components/data/data-workspace.tsx +272 -0
- package/components/data/firecrawl-notice.tsx +236 -0
- package/components/data/jobs-panel.tsx +485 -0
- package/components/data/paste-panel.tsx +79 -0
- package/components/data/scrape-panel.tsx +964 -0
- package/components/data/upload-panel.tsx +714 -0
- package/components/demo/demo-workspace.tsx +419 -0
- package/components/global-settings.tsx +286 -0
- package/components/index/index-workspace.tsx +482 -0
- package/components/onboarding/simple-setup.tsx +702 -0
- package/components/onboarding/tech-setup.tsx +121 -0
- package/components/onboarding/welcome-screen.tsx +183 -0
- package/components/page-header.tsx +44 -0
- package/components/server/code-viewer.tsx +87 -0
- package/components/server/deploy-button.tsx +1390 -0
- package/components/server/server-workspace.tsx +392 -0
- package/components/simple/sdk-connect-dialog.tsx +201 -0
- package/components/simple/simple-index-button.tsx +83 -0
- package/components/simple/simple-sidebar.tsx +161 -0
- package/components/theme-customizer-provider.tsx +171 -0
- package/components/theme-switcher.tsx +445 -0
- package/components/ui/alert-dialog.tsx +187 -0
- package/components/ui/alert.tsx +76 -0
- package/components/ui/badge.tsx +52 -0
- package/components/ui/button.tsx +59 -0
- package/components/ui/card.tsx +103 -0
- package/components/ui/checkbox.tsx +28 -0
- package/components/ui/collapsible.tsx +21 -0
- package/components/ui/dialog.tsx +159 -0
- package/components/ui/dropdown-menu.tsx +281 -0
- package/components/ui/input.tsx +22 -0
- package/components/ui/label.tsx +20 -0
- package/components/ui/popover.tsx +90 -0
- package/components/ui/progress.tsx +83 -0
- package/components/ui/provider-icon.tsx +94 -0
- package/components/ui/scroll-area.tsx +55 -0
- package/components/ui/select.tsx +248 -0
- package/components/ui/separator.tsx +25 -0
- package/components/ui/sheet.tsx +138 -0
- package/components/ui/skeleton.tsx +13 -0
- package/components/ui/sonner.tsx +63 -0
- package/components/ui/switch.tsx +32 -0
- package/components/ui/table.tsx +116 -0
- package/components/ui/tabs.tsx +84 -0
- package/components/ui/textarea.tsx +18 -0
- package/components/ui/tooltip.tsx +66 -0
- package/components/workspace/delete-server-dialog.tsx +71 -0
- package/components/workspace/server-form-dialog.tsx +103 -0
- package/components/workspace/server-switcher.tsx +163 -0
- package/components/workspace/workspace-provider.tsx +177 -0
- package/components/workspace/workspace-top-bar.tsx +23 -0
- package/components.json +21 -0
- package/lib/error-formatter.ts +15 -0
- package/lib/utils.ts +6 -0
- package/next-env.d.ts +6 -0
- package/next.config.mjs +18 -0
- package/package.json +65 -0
- package/postcss.config.mjs +8 -0
- package/public/docker.png +0 -0
- package/public/favicon.ico +0 -0
- package/public/github.svg +3 -0
- package/public/hetzner.svg +1 -0
- package/public/icons/aws.svg +38 -0
- package/public/icons/azure.svg +1 -0
- package/public/icons/chroma.png +0 -0
- package/public/icons/cohere.svg +30 -0
- package/public/icons/digital-ocean.webp +0 -0
- package/public/icons/firecrawl2.png +0 -0
- package/public/icons/gcp.svg +1 -0
- package/public/icons/gemini.svg +1 -0
- package/public/icons/jina.svg +1 -0
- package/public/icons/lancedb2.png +0 -0
- package/public/icons/mistral.svg +469 -0
- package/public/icons/nomic.png +0 -0
- package/public/icons/openai.svg +1 -0
- package/public/icons/pgvector2.png +0 -0
- package/public/icons/pinecone.png +0 -0
- package/public/icons/qdrant.svg +35 -0
- package/public/icons/supabase.png +0 -0
- package/public/icons/vercel copy.svg +1 -0
- package/public/icons/vercel.svg +1 -0
- package/public/icons/voyage-light.png +0 -0
- package/public/icons/weaviate.webp +0 -0
- package/public/logo-light.png +0 -0
- package/public/logo.png +0 -0
- package/public/logo9.png +0 -0
- package/public/vercel.svg +1 -0
- package/tsconfig.json +33 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Tabs as TabsPrimitive } from "@base-ui/react/tabs";
|
|
4
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
5
|
+
|
|
6
|
+
import { cn } from "@/lib/utils";
|
|
7
|
+
|
|
8
|
+
function Tabs({
|
|
9
|
+
className,
|
|
10
|
+
orientation = "horizontal",
|
|
11
|
+
...props
|
|
12
|
+
}: TabsPrimitive.Root.Props) {
|
|
13
|
+
return (
|
|
14
|
+
<TabsPrimitive.Root
|
|
15
|
+
data-slot="tabs"
|
|
16
|
+
data-orientation={orientation}
|
|
17
|
+
className={cn(
|
|
18
|
+
"group/tabs flex gap-2 data-horizontal:flex-col",
|
|
19
|
+
className,
|
|
20
|
+
)}
|
|
21
|
+
{...props}
|
|
22
|
+
/>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const tabsListVariants = cva(
|
|
27
|
+
"group/tabs-list inline-flex w-fit items-center justify-center rounded-lg p-[3px] text-muted-foreground group-data-horizontal/tabs:h-8 group-data-vertical/tabs:h-fit group-data-vertical/tabs:flex-col data-[variant=line]:rounded-none",
|
|
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
|
+
}: TabsPrimitive.List.Props & VariantProps<typeof tabsListVariants>) {
|
|
46
|
+
return (
|
|
47
|
+
<TabsPrimitive.List
|
|
48
|
+
data-slot="tabs-list"
|
|
49
|
+
data-variant={variant}
|
|
50
|
+
className={cn(tabsListVariants({ variant }), className)}
|
|
51
|
+
{...props}
|
|
52
|
+
/>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function TabsTrigger({ className, ...props }: TabsPrimitive.Tab.Props) {
|
|
57
|
+
return (
|
|
58
|
+
<TabsPrimitive.Tab
|
|
59
|
+
data-slot="tabs-trigger"
|
|
60
|
+
className={cn(
|
|
61
|
+
"relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-1.5 py-0.5 text-sm font-medium whitespace-nowrap text-foreground/60 transition-all group-data-vertical/tabs:w-full group-data-vertical/tabs:justify-start hover:text-foreground focus-visible:ring-[3px] focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 has-data-[icon=inline-end]:pr-1 has-data-[icon=inline-start]:pl-1 aria-disabled:pointer-events-none aria-disabled:opacity-50 dark:text-muted-foreground dark:hover:text-foreground group-data-[variant=default]/tabs-list:data-active:shadow-sm group-data-[variant=line]/tabs-list:data-active:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
62
|
+
"group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent",
|
|
63
|
+
"data-active:bg-background data-active:text-foreground dark:data-active:border-input dark:data-active:bg-input/30 dark:data-active:text-foreground",
|
|
64
|
+
"after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-horizontal/tabs:after:inset-x-0 group-data-horizontal/tabs:after:bottom-[-5px] group-data-horizontal/tabs:after:h-0.5 group-data-vertical/tabs:after:inset-y-0 group-data-vertical/tabs:after:-right-1 group-data-vertical/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100",
|
|
65
|
+
"border shadow-none! data-active:border-border cursor-pointer",
|
|
66
|
+
// "border shadow-none!",
|
|
67
|
+
className,
|
|
68
|
+
)}
|
|
69
|
+
{...props}
|
|
70
|
+
/>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function TabsContent({ className, ...props }: TabsPrimitive.Panel.Props) {
|
|
75
|
+
return (
|
|
76
|
+
<TabsPrimitive.Panel
|
|
77
|
+
data-slot="tabs-content"
|
|
78
|
+
className={cn("flex-1 text-sm outline-none", className)}
|
|
79
|
+
{...props}
|
|
80
|
+
/>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
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
|
+
"flex field-sizing-content min-h-16 w-full rounded-lg border border-input bg-transparent px-2.5 py-2 text-base transition-colors outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
|
|
11
|
+
className,
|
|
12
|
+
)}
|
|
13
|
+
{...props}
|
|
14
|
+
/>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export { Textarea };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Tooltip as TooltipPrimitive } from "@base-ui/react/tooltip";
|
|
4
|
+
|
|
5
|
+
import { cn } from "@/lib/utils";
|
|
6
|
+
|
|
7
|
+
function TooltipProvider({
|
|
8
|
+
delay = 0,
|
|
9
|
+
...props
|
|
10
|
+
}: TooltipPrimitive.Provider.Props) {
|
|
11
|
+
return (
|
|
12
|
+
<TooltipPrimitive.Provider
|
|
13
|
+
data-slot="tooltip-provider"
|
|
14
|
+
delay={delay}
|
|
15
|
+
{...props}
|
|
16
|
+
/>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function Tooltip({ ...props }: TooltipPrimitive.Root.Props) {
|
|
21
|
+
return <TooltipPrimitive.Root data-slot="tooltip" {...props} />;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function TooltipTrigger({ ...props }: TooltipPrimitive.Trigger.Props) {
|
|
25
|
+
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function TooltipContent({
|
|
29
|
+
className,
|
|
30
|
+
side = "top",
|
|
31
|
+
sideOffset = 4,
|
|
32
|
+
align = "center",
|
|
33
|
+
alignOffset = 0,
|
|
34
|
+
children,
|
|
35
|
+
...props
|
|
36
|
+
}: TooltipPrimitive.Popup.Props &
|
|
37
|
+
Pick<
|
|
38
|
+
TooltipPrimitive.Positioner.Props,
|
|
39
|
+
"align" | "alignOffset" | "side" | "sideOffset"
|
|
40
|
+
>) {
|
|
41
|
+
return (
|
|
42
|
+
<TooltipPrimitive.Portal>
|
|
43
|
+
<TooltipPrimitive.Positioner
|
|
44
|
+
align={align}
|
|
45
|
+
alignOffset={alignOffset}
|
|
46
|
+
side={side}
|
|
47
|
+
sideOffset={sideOffset}
|
|
48
|
+
className="isolate z-50"
|
|
49
|
+
>
|
|
50
|
+
<TooltipPrimitive.Popup
|
|
51
|
+
data-slot="tooltip-content"
|
|
52
|
+
className={cn(
|
|
53
|
+
"z-50 inline-flex w-fit max-w-xs origin-(--transform-origin) items-center gap-1.5 rounded-md bg-foreground px-3 py-1.5 text-xs text-background has-data-[slot=kbd]:pr-1.5 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-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 **:data-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-50 **:data-[slot=kbd]:rounded-sm data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
|
|
54
|
+
className,
|
|
55
|
+
)}
|
|
56
|
+
{...props}
|
|
57
|
+
>
|
|
58
|
+
{children}
|
|
59
|
+
<TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%-2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground data-[side=bottom]:top-1 data-[side=inline-end]:top-1/2! data-[side=inline-end]:-left-1 data-[side=inline-end]:-translate-y-1/2 data-[side=inline-start]:top-1/2! data-[side=inline-start]:-right-1 data-[side=inline-start]:-translate-y-1/2 data-[side=left]:top-1/2! data-[side=left]:-right-1 data-[side=left]:-translate-y-1/2 data-[side=right]:top-1/2! data-[side=right]:-left-1 data-[side=right]:-translate-y-1/2 data-[side=top]:-bottom-2.5" />
|
|
60
|
+
</TooltipPrimitive.Popup>
|
|
61
|
+
</TooltipPrimitive.Positioner>
|
|
62
|
+
</TooltipPrimitive.Portal>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useState } from "react"
|
|
4
|
+
import { toast } from "sonner"
|
|
5
|
+
import { Loader2 } from "lucide-react"
|
|
6
|
+
import { Button } from "@/components/ui/button"
|
|
7
|
+
import {
|
|
8
|
+
Dialog,
|
|
9
|
+
DialogContent,
|
|
10
|
+
DialogDescription,
|
|
11
|
+
DialogFooter,
|
|
12
|
+
DialogHeader,
|
|
13
|
+
DialogTitle,
|
|
14
|
+
} from "@/components/ui/dialog"
|
|
15
|
+
import { useWorkspace, type WorkspaceServer } from "./workspace-provider"
|
|
16
|
+
|
|
17
|
+
interface Props {
|
|
18
|
+
target: WorkspaceServer | null
|
|
19
|
+
open: boolean
|
|
20
|
+
onOpenChange: (open: boolean) => void
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function DeleteServerDialog({ target, open, onOpenChange }: Props) {
|
|
24
|
+
const { deleteServer } = useWorkspace()
|
|
25
|
+
const [busy, setBusy] = useState(false)
|
|
26
|
+
|
|
27
|
+
async function confirm() {
|
|
28
|
+
if (!target) return
|
|
29
|
+
setBusy(true)
|
|
30
|
+
try {
|
|
31
|
+
await deleteServer(target.id)
|
|
32
|
+
toast.success(`Deleted "${target.name}".`)
|
|
33
|
+
onOpenChange(false)
|
|
34
|
+
} catch {
|
|
35
|
+
toast.error("Could not delete the server.")
|
|
36
|
+
} finally {
|
|
37
|
+
setBusy(false)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
43
|
+
<DialogContent className="sm:max-w-md">
|
|
44
|
+
<DialogHeader>
|
|
45
|
+
<DialogTitle>Delete “{target?.name}”?</DialogTitle>
|
|
46
|
+
<DialogDescription>
|
|
47
|
+
This permanently removes the server's documents, index, vector
|
|
48
|
+
data, and generated files. This cannot be undone.
|
|
49
|
+
</DialogDescription>
|
|
50
|
+
</DialogHeader>
|
|
51
|
+
<DialogFooter>
|
|
52
|
+
<Button
|
|
53
|
+
variant="outline"
|
|
54
|
+
onClick={() => onOpenChange(false)}
|
|
55
|
+
disabled={busy}
|
|
56
|
+
>
|
|
57
|
+
Cancel
|
|
58
|
+
</Button>
|
|
59
|
+
<Button
|
|
60
|
+
variant="destructive"
|
|
61
|
+
onClick={() => void confirm()}
|
|
62
|
+
disabled={busy}
|
|
63
|
+
>
|
|
64
|
+
{busy && <Loader2 className="size-4 animate-spin" />}
|
|
65
|
+
Delete server
|
|
66
|
+
</Button>
|
|
67
|
+
</DialogFooter>
|
|
68
|
+
</DialogContent>
|
|
69
|
+
</Dialog>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react"
|
|
4
|
+
import { toast } from "sonner"
|
|
5
|
+
import { Loader2 } from "lucide-react"
|
|
6
|
+
import { Button } from "@/components/ui/button"
|
|
7
|
+
import {
|
|
8
|
+
Dialog,
|
|
9
|
+
DialogContent,
|
|
10
|
+
DialogDescription,
|
|
11
|
+
DialogFooter,
|
|
12
|
+
DialogHeader,
|
|
13
|
+
DialogTitle,
|
|
14
|
+
} from "@/components/ui/dialog"
|
|
15
|
+
import { Input } from "@/components/ui/input"
|
|
16
|
+
import { Label } from "@/components/ui/label"
|
|
17
|
+
import { useWorkspace, type WorkspaceServer } from "./workspace-provider"
|
|
18
|
+
|
|
19
|
+
interface Props {
|
|
20
|
+
mode: "create" | "rename"
|
|
21
|
+
target?: WorkspaceServer | null
|
|
22
|
+
open: boolean
|
|
23
|
+
onOpenChange: (open: boolean) => void
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function ServerFormDialog({ mode, target, open, onOpenChange }: Props) {
|
|
27
|
+
const { createServer, renameServer } = useWorkspace()
|
|
28
|
+
const [name, setName] = useState("")
|
|
29
|
+
const [busy, setBusy] = useState(false)
|
|
30
|
+
|
|
31
|
+
// Seed the field whenever the dialog opens.
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
if (open) setName(mode === "rename" ? (target?.name ?? "") : "")
|
|
34
|
+
}, [open, mode, target])
|
|
35
|
+
|
|
36
|
+
async function submit() {
|
|
37
|
+
const trimmed = name.trim()
|
|
38
|
+
if (!trimmed) {
|
|
39
|
+
toast.error("Enter a server name.")
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
setBusy(true)
|
|
43
|
+
try {
|
|
44
|
+
if (mode === "create") {
|
|
45
|
+
const created = await createServer(trimmed)
|
|
46
|
+
if (created) toast.success(`Created "${created.name}" and switched to it.`)
|
|
47
|
+
} else if (target) {
|
|
48
|
+
await renameServer(target.id, trimmed)
|
|
49
|
+
toast.success("Server renamed.")
|
|
50
|
+
}
|
|
51
|
+
onOpenChange(false)
|
|
52
|
+
} catch {
|
|
53
|
+
toast.error("Something went wrong. Try again.")
|
|
54
|
+
} finally {
|
|
55
|
+
setBusy(false)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
61
|
+
<DialogContent className="sm:max-w-md">
|
|
62
|
+
<DialogHeader>
|
|
63
|
+
<DialogTitle>
|
|
64
|
+
{mode === "create" ? "New RAG server" : "Rename server"}
|
|
65
|
+
</DialogTitle>
|
|
66
|
+
<DialogDescription>
|
|
67
|
+
{mode === "create"
|
|
68
|
+
? "Each server is an isolated RAG project with its own documents, index, and deployable artifact."
|
|
69
|
+
: "Give this server a clearer name."}
|
|
70
|
+
</DialogDescription>
|
|
71
|
+
</DialogHeader>
|
|
72
|
+
|
|
73
|
+
<div className="grid gap-2">
|
|
74
|
+
<Label htmlFor="server-name">Server name</Label>
|
|
75
|
+
<Input
|
|
76
|
+
id="server-name"
|
|
77
|
+
value={name}
|
|
78
|
+
autoFocus
|
|
79
|
+
placeholder="e.g. docs-assistant"
|
|
80
|
+
onChange={(e) => setName(e.target.value)}
|
|
81
|
+
onKeyDown={(e) => {
|
|
82
|
+
if (e.key === "Enter") void submit()
|
|
83
|
+
}}
|
|
84
|
+
/>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<DialogFooter>
|
|
88
|
+
<Button
|
|
89
|
+
variant="outline"
|
|
90
|
+
onClick={() => onOpenChange(false)}
|
|
91
|
+
disabled={busy}
|
|
92
|
+
>
|
|
93
|
+
Cancel
|
|
94
|
+
</Button>
|
|
95
|
+
<Button onClick={() => void submit()} disabled={busy}>
|
|
96
|
+
{busy && <Loader2 className="size-4 animate-spin" />}
|
|
97
|
+
{mode === "create" ? "Create server" : "Save"}
|
|
98
|
+
</Button>
|
|
99
|
+
</DialogFooter>
|
|
100
|
+
</DialogContent>
|
|
101
|
+
</Dialog>
|
|
102
|
+
)
|
|
103
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import {
|
|
5
|
+
Check,
|
|
6
|
+
ChevronsUpDown,
|
|
7
|
+
Pencil,
|
|
8
|
+
Plus,
|
|
9
|
+
Server,
|
|
10
|
+
Trash2,
|
|
11
|
+
} from "lucide-react";
|
|
12
|
+
import { cn } from "@/lib/utils";
|
|
13
|
+
import {
|
|
14
|
+
DropdownMenu,
|
|
15
|
+
DropdownMenuContent,
|
|
16
|
+
DropdownMenuGroup,
|
|
17
|
+
DropdownMenuItem,
|
|
18
|
+
DropdownMenuLabel,
|
|
19
|
+
DropdownMenuSeparator,
|
|
20
|
+
DropdownMenuTrigger,
|
|
21
|
+
} from "@/components/ui/dropdown-menu";
|
|
22
|
+
import { useWorkspace, type WorkspaceServer } from "./workspace-provider";
|
|
23
|
+
import { ServerFormDialog } from "./server-form-dialog";
|
|
24
|
+
import { DeleteServerDialog } from "./delete-server-dialog";
|
|
25
|
+
import { Separator } from "../ui/separator";
|
|
26
|
+
|
|
27
|
+
export function ServerSwitcher() {
|
|
28
|
+
const { servers, activeServer, activateServer } = useWorkspace();
|
|
29
|
+
const [createOpen, setCreateOpen] = useState(false);
|
|
30
|
+
const [renameTarget, setRenameTarget] = useState<WorkspaceServer | null>(
|
|
31
|
+
null,
|
|
32
|
+
);
|
|
33
|
+
const [deleteTarget, setDeleteTarget] = useState<WorkspaceServer | null>(
|
|
34
|
+
null,
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<>
|
|
39
|
+
<DropdownMenu>
|
|
40
|
+
<DropdownMenuTrigger
|
|
41
|
+
render={
|
|
42
|
+
<button
|
|
43
|
+
type="button"
|
|
44
|
+
className="flex min-w-[200px] md:min-w-[240px] items-center gap-2 rounded-lg border border-border bg-card px-2 py-1.5 text-left transition-colors hover:bg-accent/60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
45
|
+
/>
|
|
46
|
+
}
|
|
47
|
+
>
|
|
48
|
+
<span className="flex size-6 shrink-0 items-center justify-center rounded-md bg-primary/12 text-primary">
|
|
49
|
+
<Server className="size-3.5" />
|
|
50
|
+
</span>
|
|
51
|
+
<span className="flex min-w-0 flex-1 flex-col">
|
|
52
|
+
{/* <span className="text-[10px] font-medium uppercase tracking-wider text-muted-foreground">
|
|
53
|
+
Server
|
|
54
|
+
</span> */}
|
|
55
|
+
<span className="flex items-center gap-1.5">
|
|
56
|
+
<span className="truncate text-sm font-medium text-foreground">
|
|
57
|
+
{activeServer?.name ?? "No server"}
|
|
58
|
+
</span>
|
|
59
|
+
{activeServer?.running && <RunningDot />}
|
|
60
|
+
</span>
|
|
61
|
+
</span>
|
|
62
|
+
<ChevronsUpDown className="size-4 shrink-0 text-muted-foreground" />
|
|
63
|
+
</DropdownMenuTrigger>
|
|
64
|
+
|
|
65
|
+
<DropdownMenuContent
|
|
66
|
+
align="start"
|
|
67
|
+
sideOffset={6}
|
|
68
|
+
className="w-(--anchor-width) min-w-[260px]"
|
|
69
|
+
>
|
|
70
|
+
<DropdownMenuGroup>
|
|
71
|
+
<DropdownMenuLabel>Your servers</DropdownMenuLabel>
|
|
72
|
+
{servers.map((s) => {
|
|
73
|
+
const active = s.id === activeServer?.id;
|
|
74
|
+
return (
|
|
75
|
+
<DropdownMenuItem
|
|
76
|
+
key={s.id}
|
|
77
|
+
onClick={() => {
|
|
78
|
+
if (!active) void activateServer(s.id);
|
|
79
|
+
}}
|
|
80
|
+
className="gap-2"
|
|
81
|
+
>
|
|
82
|
+
<span className="flex size-7 shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground">
|
|
83
|
+
<Server className="size-3.5" />
|
|
84
|
+
</span>
|
|
85
|
+
<span className="flex min-w-0 flex-1 flex-col">
|
|
86
|
+
<span className="truncate font-medium">{s.name}</span>
|
|
87
|
+
<span className="flex items-center gap-2 text-[11px] text-muted-foreground tabular-nums">
|
|
88
|
+
<span>{s.docCount} docs</span>
|
|
89
|
+
{s.indexed && <span>· indexed</span>}
|
|
90
|
+
{s.running && (
|
|
91
|
+
<span className="flex items-center gap-1 text-green-500">
|
|
92
|
+
<RunningDot />
|
|
93
|
+
live
|
|
94
|
+
</span>
|
|
95
|
+
)}
|
|
96
|
+
</span>
|
|
97
|
+
</span>
|
|
98
|
+
{active && <Check className="size-4 shrink-0 text-primary" />}
|
|
99
|
+
</DropdownMenuItem>
|
|
100
|
+
);
|
|
101
|
+
})}
|
|
102
|
+
</DropdownMenuGroup>
|
|
103
|
+
|
|
104
|
+
<DropdownMenuSeparator />
|
|
105
|
+
|
|
106
|
+
<DropdownMenuItem
|
|
107
|
+
onClick={() => setCreateOpen(true)}
|
|
108
|
+
className="gap-2 h-9"
|
|
109
|
+
>
|
|
110
|
+
<Plus className="size-4" />
|
|
111
|
+
New server
|
|
112
|
+
</DropdownMenuItem>
|
|
113
|
+
{/* <DropdownMenuSeparator /> */}
|
|
114
|
+
{activeServer && (
|
|
115
|
+
<DropdownMenuItem
|
|
116
|
+
onClick={() => setRenameTarget(activeServer)}
|
|
117
|
+
className="gap-2 h-9"
|
|
118
|
+
>
|
|
119
|
+
<Pencil className="size-4" />
|
|
120
|
+
Rename current
|
|
121
|
+
</DropdownMenuItem>
|
|
122
|
+
)}
|
|
123
|
+
{activeServer && servers.length > 1 && (
|
|
124
|
+
<DropdownMenuItem
|
|
125
|
+
variant="destructive"
|
|
126
|
+
onClick={() => setDeleteTarget(activeServer)}
|
|
127
|
+
className="gap-2"
|
|
128
|
+
>
|
|
129
|
+
<Trash2 className="size-4" />
|
|
130
|
+
Delete current
|
|
131
|
+
</DropdownMenuItem>
|
|
132
|
+
)}
|
|
133
|
+
</DropdownMenuContent>
|
|
134
|
+
</DropdownMenu>
|
|
135
|
+
|
|
136
|
+
<ServerFormDialog
|
|
137
|
+
mode="create"
|
|
138
|
+
open={createOpen}
|
|
139
|
+
onOpenChange={setCreateOpen}
|
|
140
|
+
/>
|
|
141
|
+
<ServerFormDialog
|
|
142
|
+
mode="rename"
|
|
143
|
+
target={renameTarget}
|
|
144
|
+
open={renameTarget !== null}
|
|
145
|
+
onOpenChange={(o) => !o && setRenameTarget(null)}
|
|
146
|
+
/>
|
|
147
|
+
<DeleteServerDialog
|
|
148
|
+
target={deleteTarget}
|
|
149
|
+
open={deleteTarget !== null}
|
|
150
|
+
onOpenChange={(o) => !o && setDeleteTarget(null)}
|
|
151
|
+
/>
|
|
152
|
+
</>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function RunningDot() {
|
|
157
|
+
return (
|
|
158
|
+
<span className="relative flex size-2">
|
|
159
|
+
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-green-500 opacity-75" />
|
|
160
|
+
<span className="relative inline-flex size-2 rounded-full bg-green-500" />
|
|
161
|
+
</span>
|
|
162
|
+
);
|
|
163
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
createContext,
|
|
5
|
+
useCallback,
|
|
6
|
+
useContext,
|
|
7
|
+
type ReactNode,
|
|
8
|
+
} from "react"
|
|
9
|
+
import useSWR, { mutate as globalMutate } from "swr"
|
|
10
|
+
import type { WorkspaceMode } from "@larkup/core/workspace"
|
|
11
|
+
|
|
12
|
+
/** A workspace server enriched with the per-server status the UI shows. */
|
|
13
|
+
export interface WorkspaceServer {
|
|
14
|
+
id: string
|
|
15
|
+
name: string
|
|
16
|
+
port: number
|
|
17
|
+
createdAt: string
|
|
18
|
+
updatedAt: string
|
|
19
|
+
docCount: number
|
|
20
|
+
indexed: boolean
|
|
21
|
+
running: boolean
|
|
22
|
+
endpoint: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface WorkspaceData {
|
|
26
|
+
username: string | null
|
|
27
|
+
activeServerId: string | null
|
|
28
|
+
servers: WorkspaceServer[]
|
|
29
|
+
mode: WorkspaceMode | null
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface WorkspaceContextValue {
|
|
33
|
+
isLoading: boolean
|
|
34
|
+
username: string | null
|
|
35
|
+
servers: WorkspaceServer[]
|
|
36
|
+
activeServer?: WorkspaceServer
|
|
37
|
+
/** No servers exist yet → run first-time onboarding. */
|
|
38
|
+
isFirstRun: boolean
|
|
39
|
+
/** User-selected mode: tech, simple, or null (not yet chosen). */
|
|
40
|
+
mode: WorkspaceMode | null
|
|
41
|
+
refresh: () => void
|
|
42
|
+
createServer: (name: string) => Promise<WorkspaceServer | undefined>
|
|
43
|
+
activateServer: (id: string) => Promise<void>
|
|
44
|
+
renameServer: (id: string, name: string) => Promise<void>
|
|
45
|
+
deleteServer: (id: string) => Promise<void>
|
|
46
|
+
setUsername: (name: string) => Promise<void>
|
|
47
|
+
setMode: (mode: WorkspaceMode) => Promise<void>
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const fetcher = (url: string) => fetch(url).then((r) => r.json())
|
|
51
|
+
|
|
52
|
+
const WorkspaceContext = createContext<WorkspaceContextValue | null>(null)
|
|
53
|
+
|
|
54
|
+
export function useWorkspace() {
|
|
55
|
+
const ctx = useContext(WorkspaceContext)
|
|
56
|
+
if (!ctx) {
|
|
57
|
+
throw new Error("useWorkspace must be used within a WorkspaceProvider")
|
|
58
|
+
}
|
|
59
|
+
return ctx
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function WorkspaceProvider({ children }: { children: ReactNode }) {
|
|
63
|
+
const { data, isLoading, mutate } = useSWR<WorkspaceData>(
|
|
64
|
+
"/api/servers",
|
|
65
|
+
fetcher,
|
|
66
|
+
{ revalidateOnFocus: false },
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
const servers = data?.servers ?? []
|
|
70
|
+
const activeServer = servers.find((s) => s.id === data?.activeServerId)
|
|
71
|
+
const isFirstRun = !isLoading && servers.length === 0
|
|
72
|
+
|
|
73
|
+
/** Revalidate every SWR key — used after switching the active server so all
|
|
74
|
+
* stage pages reflect the newly selected server. */
|
|
75
|
+
const refreshAllStages = useCallback(async () => {
|
|
76
|
+
await globalMutate(() => true)
|
|
77
|
+
}, [])
|
|
78
|
+
|
|
79
|
+
const createServer = useCallback(
|
|
80
|
+
async (name: string) => {
|
|
81
|
+
const res = await fetch("/api/servers", {
|
|
82
|
+
method: "POST",
|
|
83
|
+
headers: { "Content-Type": "application/json" },
|
|
84
|
+
body: JSON.stringify({ name }),
|
|
85
|
+
})
|
|
86
|
+
const json = await res.json()
|
|
87
|
+
await mutate()
|
|
88
|
+
await refreshAllStages()
|
|
89
|
+
return json.server as WorkspaceServer | undefined
|
|
90
|
+
},
|
|
91
|
+
[mutate, refreshAllStages],
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
const activateServer = useCallback(
|
|
95
|
+
async (id: string) => {
|
|
96
|
+
await fetch("/api/servers", {
|
|
97
|
+
method: "PATCH",
|
|
98
|
+
headers: { "Content-Type": "application/json" },
|
|
99
|
+
body: JSON.stringify({ action: "activate", serverId: id }),
|
|
100
|
+
})
|
|
101
|
+
await mutate()
|
|
102
|
+
await refreshAllStages()
|
|
103
|
+
},
|
|
104
|
+
[mutate, refreshAllStages],
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
const renameServer = useCallback(
|
|
108
|
+
async (id: string, name: string) => {
|
|
109
|
+
await fetch("/api/servers", {
|
|
110
|
+
method: "PATCH",
|
|
111
|
+
headers: { "Content-Type": "application/json" },
|
|
112
|
+
body: JSON.stringify({ action: "rename", serverId: id, name }),
|
|
113
|
+
})
|
|
114
|
+
await mutate()
|
|
115
|
+
},
|
|
116
|
+
[mutate],
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
const deleteServer = useCallback(
|
|
120
|
+
async (id: string) => {
|
|
121
|
+
await fetch(`/api/servers?id=${encodeURIComponent(id)}`, {
|
|
122
|
+
method: "DELETE",
|
|
123
|
+
})
|
|
124
|
+
await mutate()
|
|
125
|
+
await refreshAllStages()
|
|
126
|
+
},
|
|
127
|
+
[mutate, refreshAllStages],
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
const setUsername = useCallback(
|
|
131
|
+
async (name: string) => {
|
|
132
|
+
await fetch("/api/servers", {
|
|
133
|
+
method: "PATCH",
|
|
134
|
+
headers: { "Content-Type": "application/json" },
|
|
135
|
+
body: JSON.stringify({ action: "setUsername", username: name }),
|
|
136
|
+
})
|
|
137
|
+
await mutate()
|
|
138
|
+
},
|
|
139
|
+
[mutate],
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
const setModeAction = useCallback(
|
|
143
|
+
async (mode: WorkspaceMode) => {
|
|
144
|
+
await fetch("/api/servers", {
|
|
145
|
+
method: "PATCH",
|
|
146
|
+
headers: { "Content-Type": "application/json" },
|
|
147
|
+
body: JSON.stringify({ action: "setMode", mode }),
|
|
148
|
+
})
|
|
149
|
+
await mutate()
|
|
150
|
+
},
|
|
151
|
+
[mutate],
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
const value: WorkspaceContextValue = {
|
|
155
|
+
isLoading,
|
|
156
|
+
username: data?.username ?? null,
|
|
157
|
+
servers,
|
|
158
|
+
activeServer,
|
|
159
|
+
isFirstRun,
|
|
160
|
+
mode: data?.mode ?? null,
|
|
161
|
+
refresh: () => {
|
|
162
|
+
void mutate()
|
|
163
|
+
},
|
|
164
|
+
createServer,
|
|
165
|
+
activateServer,
|
|
166
|
+
renameServer,
|
|
167
|
+
deleteServer,
|
|
168
|
+
setUsername,
|
|
169
|
+
setMode: setModeAction,
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return (
|
|
173
|
+
<WorkspaceContext.Provider value={value}>
|
|
174
|
+
{children}
|
|
175
|
+
</WorkspaceContext.Provider>
|
|
176
|
+
)
|
|
177
|
+
}
|