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,74 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import {
|
|
5
|
+
AlertDialog,
|
|
6
|
+
AlertDialogAction,
|
|
7
|
+
AlertDialogCancel,
|
|
8
|
+
AlertDialogContent,
|
|
9
|
+
AlertDialogDescription,
|
|
10
|
+
AlertDialogFooter,
|
|
11
|
+
AlertDialogHeader,
|
|
12
|
+
AlertDialogTitle,
|
|
13
|
+
} from "@/components/ui/alert-dialog";
|
|
14
|
+
import { cn } from "@/lib/utils";
|
|
15
|
+
|
|
16
|
+
export interface GenericAlertProps {
|
|
17
|
+
open: boolean;
|
|
18
|
+
onOpenChange: (open: boolean) => void;
|
|
19
|
+
title: React.ReactNode;
|
|
20
|
+
description?: React.ReactNode;
|
|
21
|
+
cancelText?: React.ReactNode;
|
|
22
|
+
actionText?: React.ReactNode;
|
|
23
|
+
onAction?: () => void;
|
|
24
|
+
variant?: "default" | "destructive";
|
|
25
|
+
icon?: React.ReactNode;
|
|
26
|
+
contentClassName?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function GenericAlert({
|
|
30
|
+
open,
|
|
31
|
+
onOpenChange,
|
|
32
|
+
title,
|
|
33
|
+
description,
|
|
34
|
+
cancelText = "Cancel",
|
|
35
|
+
actionText = "Continue",
|
|
36
|
+
onAction,
|
|
37
|
+
variant = "default",
|
|
38
|
+
icon,
|
|
39
|
+
contentClassName,
|
|
40
|
+
}: GenericAlertProps) {
|
|
41
|
+
return (
|
|
42
|
+
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
|
43
|
+
<AlertDialogContent className={contentClassName}>
|
|
44
|
+
<AlertDialogHeader>
|
|
45
|
+
<AlertDialogTitle className="flex items-center gap-2">
|
|
46
|
+
{icon}
|
|
47
|
+
{title}
|
|
48
|
+
</AlertDialogTitle>
|
|
49
|
+
{description && (
|
|
50
|
+
<AlertDialogDescription className={typeof description === "string" ? "whitespace-pre-wrap" : "space-y-2"}>
|
|
51
|
+
{description}
|
|
52
|
+
</AlertDialogDescription>
|
|
53
|
+
)}
|
|
54
|
+
</AlertDialogHeader>
|
|
55
|
+
<AlertDialogFooter>
|
|
56
|
+
<AlertDialogCancel>{cancelText}</AlertDialogCancel>
|
|
57
|
+
<AlertDialogAction
|
|
58
|
+
onClick={(e) => {
|
|
59
|
+
e.preventDefault();
|
|
60
|
+
if (onAction) onAction();
|
|
61
|
+
onOpenChange(false);
|
|
62
|
+
}}
|
|
63
|
+
className={cn(
|
|
64
|
+
variant === "destructive" &&
|
|
65
|
+
"bg-destructive text-white hover:bg-destructive/90 gap-2"
|
|
66
|
+
)}
|
|
67
|
+
>
|
|
68
|
+
{actionText}
|
|
69
|
+
</AlertDialogAction>
|
|
70
|
+
</AlertDialogFooter>
|
|
71
|
+
</AlertDialogContent>
|
|
72
|
+
</AlertDialog>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import Link from "next/link";
|
|
4
|
+
import { usePathname } from "next/navigation";
|
|
5
|
+
import {
|
|
6
|
+
FileStack,
|
|
7
|
+
FlaskConical,
|
|
8
|
+
Layers,
|
|
9
|
+
Lock,
|
|
10
|
+
type LucideIcon,
|
|
11
|
+
MessageCircle,
|
|
12
|
+
Server,
|
|
13
|
+
SlidersHorizontal,
|
|
14
|
+
} from "lucide-react";
|
|
15
|
+
import { cn } from "@/lib/utils";
|
|
16
|
+
import { STAGES, CURRENT_PHASE } from "@larkup/core/stages";
|
|
17
|
+
import type { StageId } from "@larkup/core/types";
|
|
18
|
+
import {
|
|
19
|
+
Tooltip,
|
|
20
|
+
TooltipContent,
|
|
21
|
+
TooltipProvider,
|
|
22
|
+
TooltipTrigger,
|
|
23
|
+
} from "@/components/ui/tooltip";
|
|
24
|
+
import { useThemeCustomizer, type PanelBgVariant } from "./theme-customizer-provider";
|
|
25
|
+
import { ThemeSwitcher } from "./theme-switcher";
|
|
26
|
+
|
|
27
|
+
const NAV_BG_COLORS: Record<PanelBgVariant, string | undefined> = {
|
|
28
|
+
"panel-default": undefined,
|
|
29
|
+
"panel-white": "#FFFFFF",
|
|
30
|
+
"panel-fafafa": "#F4F4F4",
|
|
31
|
+
"panel-warm": "#F7F1EA",
|
|
32
|
+
"panel-soft": "#FBFAF8",
|
|
33
|
+
"panel-silver": "#F8F8F8",
|
|
34
|
+
"panel-stone": "#F5F5F2",
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// [#CDA1FE]
|
|
38
|
+
const STAGE_ICONS: Record<StageId, LucideIcon> = {
|
|
39
|
+
configure: SlidersHorizontal,
|
|
40
|
+
data: FileStack,
|
|
41
|
+
index: Layers,
|
|
42
|
+
server: Server,
|
|
43
|
+
demo: FlaskConical,
|
|
44
|
+
chat: MessageCircle,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export function AppSidebar() {
|
|
48
|
+
const pathname = usePathname();
|
|
49
|
+
const { pageStyle, navBg } = useThemeCustomizer();
|
|
50
|
+
|
|
51
|
+
const navColor = navBg ? NAV_BG_COLORS[navBg] : undefined;
|
|
52
|
+
const navStyle = navColor ? { backgroundColor: navColor } : undefined;
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<TooltipProvider delay={150}>
|
|
56
|
+
<aside
|
|
57
|
+
className={cn(
|
|
58
|
+
"sticky top-0 hidden h-screen w-[84px] shrink-0 flex-col items-center gap-1 self-start py-3 md:flex",
|
|
59
|
+
!navColor ? "bg-background" : "",
|
|
60
|
+
pageStyle === "fused" ? "border-r border-border" : "",
|
|
61
|
+
)}
|
|
62
|
+
style={navStyle}
|
|
63
|
+
>
|
|
64
|
+
{/* Brand */}
|
|
65
|
+
<Tooltip>
|
|
66
|
+
<TooltipTrigger
|
|
67
|
+
render={
|
|
68
|
+
<Link
|
|
69
|
+
href="/configure"
|
|
70
|
+
aria-label="larkup home"
|
|
71
|
+
className="mb-2 flex size-11 items-center justify-center rounded-xl bg-primary text-primary-foreground shadow-sm transition-transform hover:scale-105"
|
|
72
|
+
/>
|
|
73
|
+
}
|
|
74
|
+
>
|
|
75
|
+
{/* <Boxes className="size-5" /> */}
|
|
76
|
+
<img src={"/logo9.png"} className="size-6.5" alt="logo" />
|
|
77
|
+
</TooltipTrigger>
|
|
78
|
+
<TooltipContent side="right" sideOffset={10}>
|
|
79
|
+
Larkup
|
|
80
|
+
</TooltipContent>
|
|
81
|
+
</Tooltip>
|
|
82
|
+
|
|
83
|
+
{/* Pipeline rail */}
|
|
84
|
+
<nav className="flex flex-1 flex-col items-center gap-1.5">
|
|
85
|
+
{STAGES.map((stage) => {
|
|
86
|
+
const Icon = STAGE_ICONS[stage.id];
|
|
87
|
+
const active =
|
|
88
|
+
pathname === stage.href ||
|
|
89
|
+
(stage.href === "/configure" && pathname === "/");
|
|
90
|
+
const locked = stage.phase > CURRENT_PHASE;
|
|
91
|
+
|
|
92
|
+
const body = (
|
|
93
|
+
<>
|
|
94
|
+
{/* active accent bar (Teams-style) */}
|
|
95
|
+
<span
|
|
96
|
+
className={cn(
|
|
97
|
+
"absolute left-0 top-1/2 h-7 w-1 -translate-y-1/2 rounded-r-full bg-primary transition-opacity",
|
|
98
|
+
active ? "opacity-100" : "opacity-0",
|
|
99
|
+
)}
|
|
100
|
+
/>
|
|
101
|
+
<span
|
|
102
|
+
className={cn(
|
|
103
|
+
"relative flex size-10 items-center justify-center rounded-xl transition-colors",
|
|
104
|
+
active
|
|
105
|
+
? "bg-white border text-primary"
|
|
106
|
+
: locked
|
|
107
|
+
? "text-muted-foreground/50"
|
|
108
|
+
: "text-foreground/70 group-hover:bg-white/90 group-hover:text-foreground",
|
|
109
|
+
)}
|
|
110
|
+
>
|
|
111
|
+
<Icon
|
|
112
|
+
className="size-[19px]"
|
|
113
|
+
strokeWidth={active ? 2.25 : 2}
|
|
114
|
+
/>
|
|
115
|
+
{locked && (
|
|
116
|
+
<span className="absolute -right-0.5 -top-0.5 flex size-3.5 items-center justify-center rounded-full bg-background">
|
|
117
|
+
<Lock className="size-2.5 text-muted-foreground" />
|
|
118
|
+
</span>
|
|
119
|
+
)}
|
|
120
|
+
</span>
|
|
121
|
+
<span
|
|
122
|
+
className={cn(
|
|
123
|
+
"text-[10.5px] leading-none tracking-tight",
|
|
124
|
+
active
|
|
125
|
+
? "font-semibold text-primary"
|
|
126
|
+
: locked
|
|
127
|
+
? "text-muted-foreground/50"
|
|
128
|
+
: "text-muted-foreground",
|
|
129
|
+
)}
|
|
130
|
+
>
|
|
131
|
+
{stage.label}
|
|
132
|
+
</span>
|
|
133
|
+
</>
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<Tooltip key={stage.id}>
|
|
138
|
+
<TooltipTrigger
|
|
139
|
+
render={
|
|
140
|
+
locked ? (
|
|
141
|
+
<div
|
|
142
|
+
aria-disabled
|
|
143
|
+
className="group relative flex w-16 cursor-not-allowed flex-col items-center gap-1.5 rounded-2xl py-2"
|
|
144
|
+
/>
|
|
145
|
+
) : (
|
|
146
|
+
<Link
|
|
147
|
+
href={stage.href}
|
|
148
|
+
aria-current={active ? "page" : undefined}
|
|
149
|
+
className="group relative flex w-16 flex-col items-center gap-1.5 rounded-2xl py-2 outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
150
|
+
/>
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
>
|
|
154
|
+
{body}
|
|
155
|
+
</TooltipTrigger>
|
|
156
|
+
<TooltipContent
|
|
157
|
+
side="right"
|
|
158
|
+
sideOffset={10}
|
|
159
|
+
className="max-w-56"
|
|
160
|
+
>
|
|
161
|
+
<div className="flex flex-col gap-0.5">
|
|
162
|
+
<span className="font-medium">
|
|
163
|
+
{stage.label}
|
|
164
|
+
{locked && (
|
|
165
|
+
<span className="ml-1.5 font-mono text-[10px] opacity-70">
|
|
166
|
+
Phase {stage.phase}
|
|
167
|
+
</span>
|
|
168
|
+
)}
|
|
169
|
+
</span>
|
|
170
|
+
<span className="text-[11px] leading-snug opacity-80">
|
|
171
|
+
{locked
|
|
172
|
+
? `Unlocks in Phase ${stage.phase}.`
|
|
173
|
+
: stage.description}
|
|
174
|
+
</span>
|
|
175
|
+
</div>
|
|
176
|
+
</TooltipContent>
|
|
177
|
+
</Tooltip>
|
|
178
|
+
);
|
|
179
|
+
})}
|
|
180
|
+
</nav>
|
|
181
|
+
|
|
182
|
+
{/* Footer — dual-mode indicator */}
|
|
183
|
+
<ThemeSwitcher floating={false} />
|
|
184
|
+
</aside>
|
|
185
|
+
</TooltipProvider>
|
|
186
|
+
);
|
|
187
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import Link from "next/link";
|
|
4
|
+
import { usePathname } from "next/navigation";
|
|
5
|
+
import {
|
|
6
|
+
FileStack,
|
|
7
|
+
FlaskConical,
|
|
8
|
+
Layers,
|
|
9
|
+
Lock,
|
|
10
|
+
type LucideIcon,
|
|
11
|
+
MessageCircle,
|
|
12
|
+
Server,
|
|
13
|
+
SlidersHorizontal,
|
|
14
|
+
} from "lucide-react";
|
|
15
|
+
import { cn } from "@/lib/utils";
|
|
16
|
+
import { STAGES, CURRENT_PHASE } from "@larkup/core/stages";
|
|
17
|
+
import type { StageId } from "@larkup/core/types";
|
|
18
|
+
import {
|
|
19
|
+
Tooltip,
|
|
20
|
+
TooltipContent,
|
|
21
|
+
TooltipProvider,
|
|
22
|
+
TooltipTrigger,
|
|
23
|
+
} from "@/components/ui/tooltip";
|
|
24
|
+
import { ServerSwitcher } from "@/components/workspace/server-switcher";
|
|
25
|
+
import { useWorkspace } from "@/components/workspace/workspace-provider";
|
|
26
|
+
import { ThemeSwitcher } from "./theme-switcher";
|
|
27
|
+
import { useThemeCustomizer, type PanelBgVariant } from "./theme-customizer-provider";
|
|
28
|
+
|
|
29
|
+
const NAV_BG_COLORS: Record<PanelBgVariant, string | undefined> = {
|
|
30
|
+
"panel-default": undefined,
|
|
31
|
+
"panel-white": "#FFFFFF",
|
|
32
|
+
"panel-fafafa": "#F4F4F4",
|
|
33
|
+
"panel-warm": "#F7F1EA",
|
|
34
|
+
"panel-soft": "#FBFAF8",
|
|
35
|
+
"panel-silver": "#F8F8F8",
|
|
36
|
+
"panel-stone": "#F5F5F2",
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const STAGE_ICONS: Record<StageId, LucideIcon> = {
|
|
40
|
+
configure: SlidersHorizontal,
|
|
41
|
+
data: FileStack,
|
|
42
|
+
index: Layers,
|
|
43
|
+
server: Server,
|
|
44
|
+
demo: FlaskConical,
|
|
45
|
+
chat: MessageCircle,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export function AppTopNav() {
|
|
49
|
+
const pathname = usePathname();
|
|
50
|
+
const { username } = useWorkspace();
|
|
51
|
+
const { navBg } = useThemeCustomizer();
|
|
52
|
+
|
|
53
|
+
const navColor = navBg ? NAV_BG_COLORS[navBg] : undefined;
|
|
54
|
+
const navStyle = navColor ? { backgroundColor: navColor } : undefined;
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<TooltipProvider delay={150}>
|
|
58
|
+
<header
|
|
59
|
+
className={cn(
|
|
60
|
+
"sticky top-0 z-50 flex h-16 shrink-0 items-center gap-4 border-b border-border px-4 md:px-6",
|
|
61
|
+
!navColor ? "bg-white" : "",
|
|
62
|
+
)}
|
|
63
|
+
style={navStyle}
|
|
64
|
+
>
|
|
65
|
+
{/* Brand */}
|
|
66
|
+
<Link
|
|
67
|
+
href="/configure"
|
|
68
|
+
aria-label="larkup home"
|
|
69
|
+
className="flex size-10 shrink-0 items-center justify-center rounded-xl bg-primary text-primary-foreground shadow-sm transition-transform hover:scale-105"
|
|
70
|
+
>
|
|
71
|
+
<img src={"/logo9.png"} className="size-5" alt="logo" />
|
|
72
|
+
</Link>
|
|
73
|
+
<span className="font-semibold tracking-tight hidden sm:inline-block">
|
|
74
|
+
Larkup
|
|
75
|
+
</span>
|
|
76
|
+
|
|
77
|
+
{/* Pipeline rail */}
|
|
78
|
+
<nav className="flex flex-1 h-full items-end pb-0 gap-2 md:gap-4 ml-4">
|
|
79
|
+
{STAGES.map((stage, index) => {
|
|
80
|
+
const Icon = STAGE_ICONS[stage.id];
|
|
81
|
+
const active =
|
|
82
|
+
pathname === stage.href ||
|
|
83
|
+
(stage.href === "/configure" && pathname === "/");
|
|
84
|
+
const locked = stage.phase > CURRENT_PHASE;
|
|
85
|
+
|
|
86
|
+
const body = (
|
|
87
|
+
<div className="flex items-center ">
|
|
88
|
+
<span
|
|
89
|
+
className={cn(
|
|
90
|
+
"flex size-9 items-center justify-center rounded-lg transition-colors",
|
|
91
|
+
active
|
|
92
|
+
? "text-primary"
|
|
93
|
+
: locked
|
|
94
|
+
? "text-muted-foreground/50"
|
|
95
|
+
: "text-muted-foreground group-hover:text-foreground",
|
|
96
|
+
)}
|
|
97
|
+
>
|
|
98
|
+
<Icon
|
|
99
|
+
className="size-[18px]"
|
|
100
|
+
strokeWidth={active ? 2.25 : 2}
|
|
101
|
+
/>
|
|
102
|
+
{locked && (
|
|
103
|
+
<span className="absolute -right-0.5 -top-0.5 flex size-3 items-center justify-center rounded-full bg-background">
|
|
104
|
+
<Lock className="size-2 text-muted-foreground" />
|
|
105
|
+
</span>
|
|
106
|
+
)}
|
|
107
|
+
</span>
|
|
108
|
+
<span
|
|
109
|
+
className={cn(
|
|
110
|
+
"hidden md:block text-[13px] tracking-tight",
|
|
111
|
+
active
|
|
112
|
+
? "font-semibold text-primary"
|
|
113
|
+
: locked
|
|
114
|
+
? "text-muted-foreground/50"
|
|
115
|
+
: "text-muted-foreground group-hover:text-foreground",
|
|
116
|
+
)}
|
|
117
|
+
>
|
|
118
|
+
{index + 1}. {stage.label}
|
|
119
|
+
</span>
|
|
120
|
+
{/* active accent bar (bottom) */}
|
|
121
|
+
<span
|
|
122
|
+
className={cn(
|
|
123
|
+
"absolute bottom-0 left-0 h-0.5 w-full bg-primary transition-opacity",
|
|
124
|
+
active
|
|
125
|
+
? "opacity-100"
|
|
126
|
+
: "opacity-0 group-hover:opacity-100",
|
|
127
|
+
)}
|
|
128
|
+
/>
|
|
129
|
+
</div>
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<Tooltip key={stage.id}>
|
|
134
|
+
<TooltipTrigger
|
|
135
|
+
render={
|
|
136
|
+
locked ? (
|
|
137
|
+
<div
|
|
138
|
+
aria-disabled
|
|
139
|
+
className="group relative flex cursor-not-allowed items-center gap-2 h-10 px-3 rounded-md transition-colors"
|
|
140
|
+
/>
|
|
141
|
+
) : (
|
|
142
|
+
<Link
|
|
143
|
+
href={stage.href}
|
|
144
|
+
aria-current={active ? "page" : undefined}
|
|
145
|
+
className={cn(
|
|
146
|
+
"group relative flex items-center gap-2 h-10 px-3 rounded-md transition-colors outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
147
|
+
active ? "bg-muted/0" : "hover:bg-muted/30",
|
|
148
|
+
)}
|
|
149
|
+
/>
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
>
|
|
153
|
+
{body}
|
|
154
|
+
</TooltipTrigger>
|
|
155
|
+
<TooltipContent
|
|
156
|
+
side="bottom"
|
|
157
|
+
sideOffset={10}
|
|
158
|
+
className="max-w-56"
|
|
159
|
+
>
|
|
160
|
+
<div className="flex flex-col gap-0.5">
|
|
161
|
+
<span className="font-medium">
|
|
162
|
+
{index + 1}. {stage.label}
|
|
163
|
+
{locked && (
|
|
164
|
+
<span className="ml-1.5 font-mono text-[10px] opacity-70">
|
|
165
|
+
Phase {stage.phase}
|
|
166
|
+
</span>
|
|
167
|
+
)}
|
|
168
|
+
</span>
|
|
169
|
+
<span className="text-[11px] leading-snug opacity-80">
|
|
170
|
+
{locked
|
|
171
|
+
? `Unlocks in Phase ${stage.phase}.`
|
|
172
|
+
: stage.description}
|
|
173
|
+
</span>
|
|
174
|
+
</div>
|
|
175
|
+
</TooltipContent>
|
|
176
|
+
</Tooltip>
|
|
177
|
+
);
|
|
178
|
+
})}
|
|
179
|
+
</nav>
|
|
180
|
+
|
|
181
|
+
{/* Right side: Workspace & Theme Customizer */}
|
|
182
|
+
<div className="flex items-center gap-3">
|
|
183
|
+
<ServerSwitcher />
|
|
184
|
+
{username && (
|
|
185
|
+
<span className="hidden text-sm text-muted-foreground md:block">
|
|
186
|
+
Hi,{" "}
|
|
187
|
+
<span className="font-medium text-foreground">{username}</span>
|
|
188
|
+
</span>
|
|
189
|
+
)}
|
|
190
|
+
<ThemeSwitcher floating={false} />{" "}
|
|
191
|
+
</div>
|
|
192
|
+
</header>
|
|
193
|
+
</TooltipProvider>
|
|
194
|
+
);
|
|
195
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from "react";
|
|
4
|
+
import { Settings, Eye, EyeOff } from "lucide-react";
|
|
5
|
+
import { toast } from "sonner";
|
|
6
|
+
import { useWorkspace } from "@/components/workspace/workspace-provider";
|
|
7
|
+
import useSWR from "swr";
|
|
8
|
+
import {
|
|
9
|
+
Select,
|
|
10
|
+
SelectContent,
|
|
11
|
+
SelectItem,
|
|
12
|
+
SelectTrigger,
|
|
13
|
+
SelectValue,
|
|
14
|
+
} from "@/components/ui/select";
|
|
15
|
+
import { PROVIDER_META, ProviderIcon } from "@/components/ui/provider-icon";
|
|
16
|
+
import {
|
|
17
|
+
Dialog,
|
|
18
|
+
DialogTrigger,
|
|
19
|
+
DialogContent,
|
|
20
|
+
DialogHeader,
|
|
21
|
+
DialogTitle,
|
|
22
|
+
DialogDescription,
|
|
23
|
+
DialogFooter,
|
|
24
|
+
DialogClose,
|
|
25
|
+
} from "../ui/dialog";
|
|
26
|
+
|
|
27
|
+
const fetcher = (url: string) => fetch(url).then((res) => res.json());
|
|
28
|
+
|
|
29
|
+
export function ChatSettingsModal() {
|
|
30
|
+
const [open, setOpen] = useState(false);
|
|
31
|
+
const { activeServer } = useWorkspace();
|
|
32
|
+
const serverId = activeServer?.id;
|
|
33
|
+
|
|
34
|
+
const { data: configData, mutate } = useSWR(
|
|
35
|
+
serverId ? `/api/config?serverId=${encodeURIComponent(serverId)}` : null,
|
|
36
|
+
fetcher,
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const config = configData?.config;
|
|
40
|
+
|
|
41
|
+
const [provider, setProvider] = useState("");
|
|
42
|
+
const [apiKey, setApiKey] = useState("");
|
|
43
|
+
const [systemPrompt, setSystemPrompt] = useState("");
|
|
44
|
+
const [isSaving, setIsSaving] = useState(false);
|
|
45
|
+
const [showApiKey, setShowApiKey] = useState(false);
|
|
46
|
+
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (config && open) {
|
|
49
|
+
setProvider(config.chatProvider || config.embeddingProvider || "openai");
|
|
50
|
+
setApiKey(config.chatApiKey || "");
|
|
51
|
+
setSystemPrompt(config.systemPrompt || "");
|
|
52
|
+
}
|
|
53
|
+
}, [config, open]);
|
|
54
|
+
|
|
55
|
+
async function handleSave(e: React.FormEvent) {
|
|
56
|
+
e.preventDefault();
|
|
57
|
+
if (!serverId || !config) return;
|
|
58
|
+
|
|
59
|
+
setIsSaving(true);
|
|
60
|
+
try {
|
|
61
|
+
const res = await fetch(
|
|
62
|
+
`/api/config?serverId=${encodeURIComponent(serverId)}`,
|
|
63
|
+
{
|
|
64
|
+
method: "PUT",
|
|
65
|
+
headers: { "Content-Type": "application/json" },
|
|
66
|
+
body: JSON.stringify({
|
|
67
|
+
...config,
|
|
68
|
+
chatProvider: provider,
|
|
69
|
+
chatApiKey: apiKey,
|
|
70
|
+
systemPrompt: systemPrompt,
|
|
71
|
+
}),
|
|
72
|
+
},
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
if (!res.ok) throw new Error("Failed to save settings");
|
|
76
|
+
|
|
77
|
+
toast.success("Chat settings saved.");
|
|
78
|
+
mutate();
|
|
79
|
+
setOpen(false);
|
|
80
|
+
} catch (err: any) {
|
|
81
|
+
toast.error(err.message || "Error saving settings");
|
|
82
|
+
} finally {
|
|
83
|
+
setIsSaving(false);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
89
|
+
<DialogTrigger
|
|
90
|
+
render={
|
|
91
|
+
<button
|
|
92
|
+
type="button"
|
|
93
|
+
title="Chat Settings"
|
|
94
|
+
className="flex h-8 w-8 items-center justify-center rounded-lg text-muted-foreground transition hover:bg-secondary hover:text-foreground"
|
|
95
|
+
>
|
|
96
|
+
<Settings className="h-4 w-4" />
|
|
97
|
+
</button>
|
|
98
|
+
}
|
|
99
|
+
/>
|
|
100
|
+
|
|
101
|
+
<DialogContent className="max-w-md">
|
|
102
|
+
<DialogHeader>
|
|
103
|
+
<DialogTitle>Chat Provider Settings</DialogTitle>
|
|
104
|
+
<DialogDescription>
|
|
105
|
+
Configure the AI provider and API key used for the Chat interface.
|
|
106
|
+
This can be different from your embedding provider.
|
|
107
|
+
</DialogDescription>
|
|
108
|
+
</DialogHeader>
|
|
109
|
+
|
|
110
|
+
<form onSubmit={handleSave} className="flex flex-col gap-4">
|
|
111
|
+
<div className="flex flex-col gap-1.5">
|
|
112
|
+
<label className="text-sm font-medium text-foreground">
|
|
113
|
+
AI Provider
|
|
114
|
+
</label>
|
|
115
|
+
<Select
|
|
116
|
+
value={provider}
|
|
117
|
+
onValueChange={(v) => setProvider(v ?? "")}
|
|
118
|
+
>
|
|
119
|
+
<SelectTrigger className="w-full">
|
|
120
|
+
<SelectValue placeholder="Select an AI Provider" />
|
|
121
|
+
</SelectTrigger>
|
|
122
|
+
<SelectContent>
|
|
123
|
+
{Object.entries(PROVIDER_META).map(([key, meta]) => (
|
|
124
|
+
<SelectItem key={key} value={key}>
|
|
125
|
+
<div className="flex items-center gap-2">
|
|
126
|
+
<ProviderIcon
|
|
127
|
+
src={meta.iconSrc}
|
|
128
|
+
alt={meta.label}
|
|
129
|
+
pillBg={meta.pillBg}
|
|
130
|
+
size={18}
|
|
131
|
+
/>
|
|
132
|
+
<span>{meta.label}</span>
|
|
133
|
+
</div>
|
|
134
|
+
</SelectItem>
|
|
135
|
+
))}
|
|
136
|
+
</SelectContent>
|
|
137
|
+
</Select>
|
|
138
|
+
</div>
|
|
139
|
+
|
|
140
|
+
<div className="flex flex-col gap-1.5">
|
|
141
|
+
<label className="text-sm font-medium text-foreground">
|
|
142
|
+
API Key
|
|
143
|
+
</label>
|
|
144
|
+
<div className="relative flex items-center">
|
|
145
|
+
<input
|
|
146
|
+
type={showApiKey ? "text" : "password"}
|
|
147
|
+
value={apiKey}
|
|
148
|
+
onChange={(e) => setApiKey(e.target.value)}
|
|
149
|
+
placeholder="Leave blank to use environment variable"
|
|
150
|
+
className="w-full rounded-lg border border-border bg-card px-3 py-2 pr-10 text-sm text-foreground outline-none transition focus:border-primary focus:ring-1 focus:ring-primary"
|
|
151
|
+
/>
|
|
152
|
+
<button
|
|
153
|
+
type="button"
|
|
154
|
+
onClick={() => setShowApiKey(!showApiKey)}
|
|
155
|
+
className="absolute right-3 text-muted-foreground hover:text-foreground"
|
|
156
|
+
>
|
|
157
|
+
{showApiKey ? (
|
|
158
|
+
<EyeOff className="h-4 w-4" />
|
|
159
|
+
) : (
|
|
160
|
+
<Eye className="h-4 w-4" />
|
|
161
|
+
)}
|
|
162
|
+
</button>
|
|
163
|
+
</div>
|
|
164
|
+
<p className="text-xs text-muted-foreground">
|
|
165
|
+
If left blank, it falls back to <code>AI_GATEWAY_API_KEY</code> or{" "}
|
|
166
|
+
<code>EMBEDDING_API_KEY</code> in your environment.
|
|
167
|
+
</p>
|
|
168
|
+
</div>
|
|
169
|
+
|
|
170
|
+
<div className="flex flex-col gap-1.5">
|
|
171
|
+
<label className="text-sm font-medium text-foreground">
|
|
172
|
+
System Prompt
|
|
173
|
+
</label>
|
|
174
|
+
<textarea
|
|
175
|
+
value={systemPrompt}
|
|
176
|
+
onChange={(e) => setSystemPrompt(e.target.value)}
|
|
177
|
+
placeholder="Leave blank to use default system prompt"
|
|
178
|
+
className="w-full min-h-[100px] rounded-lg border border-border bg-card px-3 py-2 text-sm text-foreground outline-none transition focus:border-primary focus:ring-1 focus:ring-primary"
|
|
179
|
+
/>
|
|
180
|
+
<p className="text-xs text-muted-foreground">
|
|
181
|
+
Override the default instructions given to the agent.
|
|
182
|
+
</p>
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
<DialogFooter className="mt-4">
|
|
186
|
+
<DialogClose
|
|
187
|
+
render={
|
|
188
|
+
<button
|
|
189
|
+
type="button"
|
|
190
|
+
className="rounded-lg px-4 py-2 text-sm font-medium text-muted-foreground transition hover:bg-secondary hover:text-foreground"
|
|
191
|
+
>
|
|
192
|
+
Cancel
|
|
193
|
+
</button>
|
|
194
|
+
}
|
|
195
|
+
/>
|
|
196
|
+
|
|
197
|
+
<button
|
|
198
|
+
type="submit"
|
|
199
|
+
disabled={isSaving}
|
|
200
|
+
className="rounded-lg bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition hover:bg-primary/90 disabled:opacity-50"
|
|
201
|
+
>
|
|
202
|
+
{isSaving ? "Saving..." : "Save Settings"}
|
|
203
|
+
</button>
|
|
204
|
+
</DialogFooter>
|
|
205
|
+
</form>
|
|
206
|
+
</DialogContent>
|
|
207
|
+
</Dialog>
|
|
208
|
+
);
|
|
209
|
+
}
|