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.
Files changed (141) hide show
  1. package/.env +2 -0
  2. package/app/actions/vercel.ts +417 -0
  3. package/app/api/chat/route.ts +192 -0
  4. package/app/api/chat/status/route.ts +55 -0
  5. package/app/api/chat/suggestions/route.ts +115 -0
  6. package/app/api/config/route.ts +73 -0
  7. package/app/api/config/test/route.ts +67 -0
  8. package/app/api/config/test-embedding/route.ts +40 -0
  9. package/app/api/config/test-llm/route.ts +47 -0
  10. package/app/api/config/test-provider/route.ts +36 -0
  11. package/app/api/demo/route.ts +176 -0
  12. package/app/api/deploy/ssh/route.ts +340 -0
  13. package/app/api/documents/route.ts +90 -0
  14. package/app/api/firecrawl/local/route.ts +46 -0
  15. package/app/api/index/route.ts +113 -0
  16. package/app/api/jobs/[id]/route.ts +44 -0
  17. package/app/api/jobs/route.ts +116 -0
  18. package/app/api/parse-file/route.ts +39 -0
  19. package/app/api/search/google/route.ts +162 -0
  20. package/app/api/search/route.ts +29 -0
  21. package/app/api/server/download/route.ts +112 -0
  22. package/app/api/server/generate/route.ts +46 -0
  23. package/app/api/server/local/route.ts +42 -0
  24. package/app/api/servers/route.ts +137 -0
  25. package/app/api/vector-stores/install/route.ts +65 -0
  26. package/app/api/vector-stores/status/route.ts +26 -0
  27. package/app/chat/page.tsx +15 -0
  28. package/app/configure/page.tsx +53 -0
  29. package/app/data/page.tsx +15 -0
  30. package/app/demo/page.tsx +15 -0
  31. package/app/favicon.ico +0 -0
  32. package/app/globals.css +190 -0
  33. package/app/index-data/page.tsx +15 -0
  34. package/app/layout.tsx +51 -0
  35. package/app/page.tsx +34 -0
  36. package/app/server/page.tsx +22 -0
  37. package/app/simple/chat/page.tsx +17 -0
  38. package/app/simple/docs/page.tsx +21 -0
  39. package/app/simple/layout.tsx +9 -0
  40. package/app/simple/settings/page.tsx +627 -0
  41. package/app/template.tsx +16 -0
  42. package/app/themes.css +733 -0
  43. package/components/alerts/generic-alert.tsx +74 -0
  44. package/components/app-sidebar.tsx +187 -0
  45. package/components/app-topnav.tsx +195 -0
  46. package/components/chat/chat-settings-modal.tsx +209 -0
  47. package/components/chat/chat-workspace.tsx +348 -0
  48. package/components/chat/knowledge-base-result.tsx +97 -0
  49. package/components/chat/message-item.tsx +103 -0
  50. package/components/client-layout-wrapper.tsx +182 -0
  51. package/components/configure/configure-form.tsx +1683 -0
  52. package/components/configure/store-fields.tsx +152 -0
  53. package/components/data/corpus-panel.tsx +499 -0
  54. package/components/data/data-workspace.tsx +272 -0
  55. package/components/data/firecrawl-notice.tsx +236 -0
  56. package/components/data/jobs-panel.tsx +485 -0
  57. package/components/data/paste-panel.tsx +79 -0
  58. package/components/data/scrape-panel.tsx +964 -0
  59. package/components/data/upload-panel.tsx +714 -0
  60. package/components/demo/demo-workspace.tsx +419 -0
  61. package/components/global-settings.tsx +286 -0
  62. package/components/index/index-workspace.tsx +482 -0
  63. package/components/onboarding/simple-setup.tsx +702 -0
  64. package/components/onboarding/tech-setup.tsx +121 -0
  65. package/components/onboarding/welcome-screen.tsx +183 -0
  66. package/components/page-header.tsx +44 -0
  67. package/components/server/code-viewer.tsx +87 -0
  68. package/components/server/deploy-button.tsx +1390 -0
  69. package/components/server/server-workspace.tsx +392 -0
  70. package/components/simple/sdk-connect-dialog.tsx +201 -0
  71. package/components/simple/simple-index-button.tsx +83 -0
  72. package/components/simple/simple-sidebar.tsx +161 -0
  73. package/components/theme-customizer-provider.tsx +171 -0
  74. package/components/theme-switcher.tsx +445 -0
  75. package/components/ui/alert-dialog.tsx +187 -0
  76. package/components/ui/alert.tsx +76 -0
  77. package/components/ui/badge.tsx +52 -0
  78. package/components/ui/button.tsx +59 -0
  79. package/components/ui/card.tsx +103 -0
  80. package/components/ui/checkbox.tsx +28 -0
  81. package/components/ui/collapsible.tsx +21 -0
  82. package/components/ui/dialog.tsx +159 -0
  83. package/components/ui/dropdown-menu.tsx +281 -0
  84. package/components/ui/input.tsx +22 -0
  85. package/components/ui/label.tsx +20 -0
  86. package/components/ui/popover.tsx +90 -0
  87. package/components/ui/progress.tsx +83 -0
  88. package/components/ui/provider-icon.tsx +94 -0
  89. package/components/ui/scroll-area.tsx +55 -0
  90. package/components/ui/select.tsx +248 -0
  91. package/components/ui/separator.tsx +25 -0
  92. package/components/ui/sheet.tsx +138 -0
  93. package/components/ui/skeleton.tsx +13 -0
  94. package/components/ui/sonner.tsx +63 -0
  95. package/components/ui/switch.tsx +32 -0
  96. package/components/ui/table.tsx +116 -0
  97. package/components/ui/tabs.tsx +84 -0
  98. package/components/ui/textarea.tsx +18 -0
  99. package/components/ui/tooltip.tsx +66 -0
  100. package/components/workspace/delete-server-dialog.tsx +71 -0
  101. package/components/workspace/server-form-dialog.tsx +103 -0
  102. package/components/workspace/server-switcher.tsx +163 -0
  103. package/components/workspace/workspace-provider.tsx +177 -0
  104. package/components/workspace/workspace-top-bar.tsx +23 -0
  105. package/components.json +21 -0
  106. package/lib/error-formatter.ts +15 -0
  107. package/lib/utils.ts +6 -0
  108. package/next-env.d.ts +6 -0
  109. package/next.config.mjs +18 -0
  110. package/package.json +65 -0
  111. package/postcss.config.mjs +8 -0
  112. package/public/docker.png +0 -0
  113. package/public/favicon.ico +0 -0
  114. package/public/github.svg +3 -0
  115. package/public/hetzner.svg +1 -0
  116. package/public/icons/aws.svg +38 -0
  117. package/public/icons/azure.svg +1 -0
  118. package/public/icons/chroma.png +0 -0
  119. package/public/icons/cohere.svg +30 -0
  120. package/public/icons/digital-ocean.webp +0 -0
  121. package/public/icons/firecrawl2.png +0 -0
  122. package/public/icons/gcp.svg +1 -0
  123. package/public/icons/gemini.svg +1 -0
  124. package/public/icons/jina.svg +1 -0
  125. package/public/icons/lancedb2.png +0 -0
  126. package/public/icons/mistral.svg +469 -0
  127. package/public/icons/nomic.png +0 -0
  128. package/public/icons/openai.svg +1 -0
  129. package/public/icons/pgvector2.png +0 -0
  130. package/public/icons/pinecone.png +0 -0
  131. package/public/icons/qdrant.svg +35 -0
  132. package/public/icons/supabase.png +0 -0
  133. package/public/icons/vercel copy.svg +1 -0
  134. package/public/icons/vercel.svg +1 -0
  135. package/public/icons/voyage-light.png +0 -0
  136. package/public/icons/weaviate.webp +0 -0
  137. package/public/logo-light.png +0 -0
  138. package/public/logo.png +0 -0
  139. package/public/logo9.png +0 -0
  140. package/public/vercel.svg +1 -0
  141. 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
+ }