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,115 @@
1
+ import { NextResponse } from "next/server";
2
+ import { readConfig, writeConfig } from "@larkup/core/config-store";
3
+ import { readRun } from "@larkup/core/index-store";
4
+ import { runWithServer } from "@larkup/core/workspace";
5
+ import { createAdapter } from "@larkup/vector-stores/factory";
6
+ import { embedQuery } from "@larkup/core/indexing/embedder";
7
+ import { generateObject } from "ai";
8
+ import { z } from "zod";
9
+ import { getChatModel } from "@larkup/core/chat-models/registry";
10
+ import { createOpenAI } from "@ai-sdk/openai";
11
+ import { createGoogleGenerativeAI } from "@ai-sdk/google";
12
+ import { createCohere } from "@ai-sdk/cohere";
13
+ import { createMistral } from "@ai-sdk/mistral";
14
+ import { createDeepSeek } from "@ai-sdk/deepseek";
15
+ import { createGateway } from "@ai-sdk/gateway";
16
+
17
+ export const dynamic = "force-dynamic";
18
+
19
+ function withServer<T>(serverId: string | null, fn: () => Promise<T>) {
20
+ return serverId ? runWithServer(serverId, fn) : fn();
21
+ }
22
+
23
+ function createChatModel(provider: string, modelId: string, apiKey?: string) {
24
+ const modelName = modelId.includes("/")
25
+ ? modelId.split("/").slice(1).join("/")
26
+ : modelId;
27
+ const key = apiKey;
28
+
29
+ switch (provider) {
30
+ case "google":
31
+ return createGoogleGenerativeAI({ apiKey: key })(modelName);
32
+ case "cohere":
33
+ return createCohere({ apiKey: key })(modelName);
34
+ case "mistral":
35
+ return createMistral({ apiKey: key })(modelName);
36
+ case "deepseek":
37
+ return createDeepSeek({ apiKey: key })(modelName);
38
+ case "vercel_ai_gateway":
39
+ return createGateway({ apiKey: key })(modelId);
40
+ case "openai":
41
+ default:
42
+ return createOpenAI({ apiKey: key })(modelName);
43
+ }
44
+ }
45
+
46
+ export async function POST(req: Request) {
47
+ const serverId = new URL(req.url).searchParams.get("serverId");
48
+ return withServer(serverId, async () => {
49
+ const config = await readConfig();
50
+ const run = await readRun();
51
+
52
+ if (config.chatSuggestions && config.chatSuggestions.length > 0) {
53
+ return NextResponse.json({ suggestions: config.chatSuggestions });
54
+ }
55
+
56
+ const indexed = run?.status === "completed" && (run.totalChunks ?? 0) > 0;
57
+ if (!indexed) {
58
+ return NextResponse.json({
59
+ suggestions: [
60
+ "What is this corpus about?",
61
+ "Summarize the key concepts",
62
+ ],
63
+ });
64
+ }
65
+
66
+ try {
67
+ // Pull a dummy vector (just embedded "summary" or similar) to get some chunks
68
+ const vector = await embedQuery(config, "overview summary");
69
+ const adapter = await createAdapter(config);
70
+ const hits = await adapter.query(vector, 5, "overview summary");
71
+
72
+ const context = hits
73
+ ?.map((h) => h.text)
74
+ ?.join("\n\n")
75
+ ?.slice(0, 3000);
76
+
77
+ const provider = config.chatProvider || config.embeddingProvider;
78
+ const chatModelId = config.chatModelId || "openai/gpt-4o-mini";
79
+ const descriptor = getChatModel(chatModelId);
80
+ const resolvedProvider =
81
+ provider === "vercel_ai_gateway"
82
+ ? "vercel_ai_gateway"
83
+ : descriptor?.provider || provider;
84
+
85
+ const aiModel = createChatModel(
86
+ resolvedProvider,
87
+ chatModelId,
88
+ config.chatApiKey || config.embeddingApiKey,
89
+ );
90
+
91
+ const { object } = await generateObject({
92
+ model: aiModel,
93
+ system:
94
+ "You are an AI that suggests 3 very short and concise questions a user could ask based on the provided text.",
95
+ prompt: `Text:\n${context}`,
96
+ schema: z.object({
97
+ suggestions: z.array(z.string()).length(3),
98
+ }),
99
+ });
100
+
101
+ config.chatSuggestions = object.suggestions;
102
+ await writeConfig(config);
103
+
104
+ return NextResponse.json({ suggestions: object.suggestions });
105
+ } catch (e: any) {
106
+ console.error("Failed to generate suggestions:", e.message);
107
+ return NextResponse.json({
108
+ suggestions: [
109
+ "What is this corpus about?",
110
+ "Summarize the key concepts",
111
+ ],
112
+ });
113
+ }
114
+ });
115
+ }
@@ -0,0 +1,73 @@
1
+ import { NextResponse } from "next/server"
2
+ import { readConfig, writeConfig } from "@larkup/core/config-store"
3
+ import { getVectorStore, validateStoreConfig } from "@larkup/vector-stores/registry"
4
+ import { getEmbeddingModel } from "@larkup/core/embeddings/registry"
5
+ import { runWithServer } from "@larkup/core/workspace"
6
+ import type { RagConfig } from "@larkup/core/types"
7
+
8
+ // Uses node:fs — must run on the Node.js runtime, not edge.
9
+ export const runtime = "nodejs"
10
+ export const dynamic = "force-dynamic"
11
+
12
+ function withServer<T>(serverId: string | null, fn: () => Promise<T>) {
13
+ return serverId ? runWithServer(serverId, fn) : fn()
14
+ }
15
+
16
+ export async function GET(request: Request) {
17
+ const serverId = new URL(request.url).searchParams.get("serverId")
18
+ return withServer(serverId, async () => {
19
+ const config = await readConfig()
20
+ return NextResponse.json({ config })
21
+ })
22
+ }
23
+
24
+ export async function PUT(request: Request) {
25
+ const serverId = new URL(request.url).searchParams.get("serverId")
26
+ let body: RagConfig
27
+ try {
28
+ body = (await request.json()) as RagConfig
29
+ } catch {
30
+ return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 })
31
+ }
32
+
33
+ // Validate the embedding model exists.
34
+ const isCustom = body.embeddingModelId.startsWith("custom:")
35
+ if (!isCustom && !getEmbeddingModel(body.embeddingModelId)) {
36
+ return NextResponse.json(
37
+ { error: `Unknown embedding model: ${body.embeddingModelId}` },
38
+ { status: 400 },
39
+ )
40
+ }
41
+ if (isCustom) {
42
+ const modelName = body.embeddingModelId.slice("custom:".length)
43
+ const found = body.customEmbeddings?.find((m) => m.modelName === modelName)
44
+ if (!found) {
45
+ return NextResponse.json(
46
+ { error: `Custom embedding model "${modelName}" not found in customEmbeddings` },
47
+ { status: 400 },
48
+ )
49
+ }
50
+ }
51
+
52
+ // Validate the store + its dynamic, store-specific fields.
53
+ const store = getVectorStore(body.vectorStore)
54
+ if (!store) {
55
+ return NextResponse.json(
56
+ { error: `Unknown vector store: ${body.vectorStore}` },
57
+ { status: 400 },
58
+ )
59
+ }
60
+ const fieldErrors = validateStoreConfig(store, body.storeConfig ?? {}, body.indexType)
61
+ if (Object.keys(fieldErrors).length > 0) {
62
+ return NextResponse.json(
63
+ { error: "Missing required vector store fields", fieldErrors },
64
+ { status: 422 },
65
+ )
66
+ }
67
+
68
+ return withServer(serverId, async () => {
69
+ const saved = await writeConfig(body)
70
+ return NextResponse.json({ config: saved })
71
+ })
72
+ }
73
+
@@ -0,0 +1,67 @@
1
+ import { NextResponse } from "next/server"
2
+ import { getVectorStore, validateStoreConfig } from "@larkup/vector-stores/registry"
3
+ import { createAdapter } from "@larkup/vector-stores/factory"
4
+ import type { RagConfig } from "@larkup/core/types"
5
+
6
+ // Uses node:fs — must run on the Node.js runtime, not edge.
7
+ export const runtime = "nodejs"
8
+
9
+ export async function POST(request: Request) {
10
+ let body: RagConfig
11
+ try {
12
+ body = (await request.json()) as RagConfig
13
+ } catch {
14
+ return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 })
15
+ }
16
+
17
+ // Validate the store + its dynamic, store-specific fields.
18
+ const store = getVectorStore(body.vectorStore)
19
+ if (!store) {
20
+ return NextResponse.json(
21
+ { error: `Unknown vector store: ${body.vectorStore}` },
22
+ { status: 400 },
23
+ )
24
+ }
25
+
26
+ const fieldErrors = validateStoreConfig(store, body.storeConfig ?? {}, body.indexType)
27
+ if (Object.keys(fieldErrors).length > 0) {
28
+ return NextResponse.json(
29
+ { error: "Missing required vector store fields", fieldErrors },
30
+ { status: 422 },
31
+ )
32
+ }
33
+
34
+ let dimensions = 1536
35
+ const isCustom = body.embeddingModelId.startsWith("custom:")
36
+ if (isCustom) {
37
+ const modelName = body.embeddingModelId.slice("custom:".length)
38
+ const found = body.customEmbeddings?.find((m) => m.modelName === modelName)
39
+ if (!found) {
40
+ return NextResponse.json(
41
+ { error: `Custom embedding model "${modelName}" not found in customEmbeddings` },
42
+ { status: 400 },
43
+ )
44
+ }
45
+ dimensions = found.dimensions
46
+ } else {
47
+ const embeddingModel = await import("@larkup/core/embeddings/registry").then(m => m.getEmbeddingModel(body.embeddingModelId))
48
+ if (!embeddingModel) {
49
+ return NextResponse.json(
50
+ { error: `Unknown embedding model: ${body.embeddingModelId}` },
51
+ { status: 400 },
52
+ )
53
+ }
54
+ dimensions = embeddingModel.dimensions
55
+ }
56
+
57
+ try {
58
+ const adapter = await createAdapter(body)
59
+ await adapter.testConnection(dimensions)
60
+ return NextResponse.json({ success: true })
61
+ } catch (error) {
62
+ return NextResponse.json(
63
+ { error: error instanceof Error ? error.message : "Connection failed" },
64
+ { status: 400 },
65
+ )
66
+ }
67
+ }
@@ -0,0 +1,40 @@
1
+ import { NextResponse } from "next/server"
2
+ import { createOpenAI } from "@ai-sdk/openai"
3
+ import { embed } from "ai"
4
+
5
+ export const runtime = "nodejs"
6
+
7
+ export async function POST(request: Request) {
8
+ let body: { baseUrl: string; apiKey?: string; modelName: string }
9
+ try {
10
+ body = await request.json()
11
+ } catch {
12
+ return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 })
13
+ }
14
+
15
+ const { baseUrl, apiKey, modelName } = body
16
+
17
+ if (!baseUrl || !modelName) {
18
+ return NextResponse.json(
19
+ { error: "Missing required fields: baseUrl and modelName" },
20
+ { status: 400 },
21
+ )
22
+ }
23
+
24
+ try {
25
+ const customProvider = createOpenAI({
26
+ baseURL: baseUrl,
27
+ apiKey: apiKey || "empty", // some backends require any string
28
+ })
29
+
30
+ const model = customProvider.embedding(modelName)
31
+ const { embedding } = await embed({ model, value: "test connection" })
32
+
33
+ return NextResponse.json({ success: true, dimensions: embedding.length })
34
+ } catch (error) {
35
+ return NextResponse.json(
36
+ { error: error instanceof Error ? error.message : "Embedding connection failed" },
37
+ { status: 400 },
38
+ )
39
+ }
40
+ }
@@ -0,0 +1,47 @@
1
+ import { NextResponse } from "next/server";
2
+ import { createOpenAI } from "@ai-sdk/openai";
3
+ import { generateText } from "ai";
4
+
5
+ export const runtime = "nodejs";
6
+
7
+ export async function POST(request: Request) {
8
+ let body: { baseUrl: string; apiKey?: string; modelName: string };
9
+ try {
10
+ body = await request.json();
11
+ } catch {
12
+ return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 });
13
+ }
14
+
15
+ const { baseUrl, apiKey, modelName } = body;
16
+
17
+ if (!baseUrl || !modelName) {
18
+ return NextResponse.json(
19
+ { error: "Missing required fields: baseUrl and modelName" },
20
+ { status: 400 },
21
+ );
22
+ }
23
+
24
+ try {
25
+ const customProvider = createOpenAI({
26
+ baseURL: baseUrl,
27
+ apiKey: apiKey || "empty", // some backends require any string
28
+ });
29
+
30
+ const model = customProvider.chat(modelName);
31
+ // We just need a simple prompt to verify connection
32
+ const { text } = await generateText({
33
+ model,
34
+ prompt: "Reply with the word 'OK'",
35
+ maxOutputTokens: 5,
36
+ });
37
+
38
+ return NextResponse.json({ success: true, text });
39
+ } catch (error) {
40
+ return NextResponse.json(
41
+ {
42
+ error: error instanceof Error ? error.message : "LLM connection failed",
43
+ },
44
+ { status: 400 },
45
+ );
46
+ }
47
+ }
@@ -0,0 +1,36 @@
1
+ import { NextResponse } from "next/server"
2
+ import { embed } from "ai"
3
+ import { getAIModel } from "@larkup/core/embeddings/providers"
4
+ import type { RagConfig } from "@larkup/core/types"
5
+
6
+ export const runtime = "nodejs"
7
+
8
+ export async function POST(request: Request) {
9
+ let body: Partial<RagConfig>
10
+ try {
11
+ body = await request.json()
12
+ } catch {
13
+ return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 })
14
+ }
15
+
16
+ const { embeddingProvider, embeddingModelId } = body
17
+
18
+ if (!embeddingProvider || !embeddingModelId) {
19
+ return NextResponse.json(
20
+ { error: "Missing required fields: embeddingProvider or embeddingModelId" },
21
+ { status: 400 },
22
+ )
23
+ }
24
+
25
+ try {
26
+ const model = getAIModel(body as RagConfig)
27
+ const { embedding } = await embed({ model, value: "test connection" })
28
+
29
+ return NextResponse.json({ success: true, dimensions: embedding.length })
30
+ } catch (error) {
31
+ return NextResponse.json(
32
+ { error: error instanceof Error ? error.message : "Connection failed. Please check your API key." },
33
+ { status: 400 },
34
+ )
35
+ }
36
+ }
@@ -0,0 +1,176 @@
1
+ import { NextResponse } from "next/server";
2
+ import { readConfig } from "@larkup/core/config-store";
3
+ import { readRun } from "@larkup/core/index-store";
4
+ import { refreshServerStatus } from "@larkup/core/generator/server-runtime";
5
+ import { createAdapter } from "@larkup/vector-stores/factory";
6
+ import { getEmbeddingModel } from "@larkup/core/embeddings/registry";
7
+ import { getVectorStore } from "@larkup/vector-stores/registry";
8
+ import type { QueryHit } from "@larkup/vector-stores/adapter";
9
+ import { runWithServer } from "@larkup/core/workspace";
10
+ import { embedQuery } from "@larkup/core/indexing/embedder";
11
+
12
+ export const dynamic = "force-dynamic";
13
+
14
+ /** Run a handler against a specific server when `serverId` is provided. */
15
+ function withServer<T>(serverId: string | null, fn: () => Promise<T>) {
16
+ return serverId ? runWithServer(serverId, fn) : fn();
17
+ }
18
+
19
+ /**
20
+ * GET /api/demo — readiness snapshot for the Demo stage.
21
+ *
22
+ * Reports whether there's a built index to query and whether a generated
23
+ * server is live, so the UI can label which path a query will take. Accepts
24
+ * `?serverId=` to inspect a specific server instead of the active one.
25
+ */
26
+ export async function GET(req: Request) {
27
+ const serverId = new URL(req.url).searchParams.get("serverId");
28
+ return withServer(serverId, () => snapshot());
29
+ }
30
+
31
+ async function snapshot() {
32
+ const config = await readConfig();
33
+ const run = await readRun();
34
+ const server = await refreshServerStatus();
35
+
36
+ const indexed = run?.status === "completed" && (run.totalChunks ?? 0) > 0;
37
+ const ready = indexed || server.running;
38
+
39
+ const blockers: string[] = [];
40
+ if (!ready) {
41
+ blockers.push(
42
+ "Build a larkup index (or launch a generated server) before running a demo query.",
43
+ );
44
+ }
45
+
46
+ return NextResponse.json({
47
+ ready,
48
+ blockers,
49
+ indexed,
50
+ server: { running: server.running, endpoint: server.endpoint },
51
+ config: {
52
+ projectName: config.projectName,
53
+ embeddingModelId: config.embeddingModelId,
54
+ vectorStore: config.vectorStore,
55
+ indexType: config.indexType,
56
+ topK: config.topK,
57
+ },
58
+ });
59
+ }
60
+
61
+ interface DemoResult {
62
+ query: string;
63
+ hits: QueryHit[];
64
+ source: "server" | "direct";
65
+ endpoint?: string;
66
+ tookMs: number;
67
+ }
68
+
69
+ /**
70
+ * POST /api/demo — run a retrieval query.
71
+ *
72
+ * Prefers a running generated server (the artifact you'd deploy), so the demo
73
+ * exercises the real serving path. Falls back to in-process retrieval against
74
+ * the toolkit's own store when no server is running.
75
+ */
76
+ export async function POST(req: Request) {
77
+ const started = Date.now();
78
+
79
+ let body: { query?: string; topK?: number; serverId?: string };
80
+ try {
81
+ body = await req.json();
82
+ } catch {
83
+ return NextResponse.json({ error: "Invalid JSON body." }, { status: 400 });
84
+ }
85
+
86
+ const query = body.query?.trim();
87
+ if (!query) {
88
+ return NextResponse.json(
89
+ { error: "A non-empty 'query' is required." },
90
+ { status: 400 },
91
+ );
92
+ }
93
+
94
+ return withServer(body.serverId ?? null, () =>
95
+ runQuery(query, body.topK, started),
96
+ );
97
+ }
98
+
99
+ async function runQuery(
100
+ query: string,
101
+ requestedTopK: number | undefined,
102
+ started: number,
103
+ ) {
104
+ const config = await readConfig();
105
+ const topK = Math.min(Math.max(Number(requestedTopK) || config.topK, 1), 20);
106
+
107
+ // 1) Prefer the running generated server — the real serving path.
108
+ const server = await refreshServerStatus();
109
+ if (server.running) {
110
+ try {
111
+ const res = await fetch(`${server.endpoint}/query`, {
112
+ method: "POST",
113
+ headers: { "Content-Type": "application/json" },
114
+ body: JSON.stringify({ query, topK }),
115
+ signal: AbortSignal.timeout(30_000),
116
+ });
117
+ const data = await res.json();
118
+ if (res.ok) {
119
+ const result: DemoResult = {
120
+ query,
121
+ hits: data.hits ?? [],
122
+ source: "server",
123
+ endpoint: server.endpoint,
124
+ tookMs: Date.now() - started,
125
+ };
126
+ return NextResponse.json(result);
127
+ }
128
+ // Fall through to direct retrieval if the server errored.
129
+ } catch {
130
+ // Server unreachable — fall back to direct retrieval below.
131
+ }
132
+ }
133
+
134
+ // 2) Fallback: in-process retrieval against the toolkit's own store.
135
+ const run = await readRun();
136
+ if (!run || run.status !== "completed" || (run.totalChunks ?? 0) === 0) {
137
+ return NextResponse.json(
138
+ {
139
+ error:
140
+ "No index to query yet. Build an index in the Index stage, or launch a server first.",
141
+ },
142
+ { status: 409 },
143
+ );
144
+ }
145
+
146
+ if (!getEmbeddingModel(config.embeddingModelId)) {
147
+ return NextResponse.json(
148
+ { error: `Unknown embedding model "${config.embeddingModelId}".` },
149
+ { status: 400 },
150
+ );
151
+ }
152
+ if (!getVectorStore(config.vectorStore)) {
153
+ return NextResponse.json(
154
+ { error: `Unknown vector store "${config.vectorStore}".` },
155
+ { status: 400 },
156
+ );
157
+ }
158
+
159
+ try {
160
+ const vector = await embedQuery(config, query);
161
+ const adapter = await createAdapter(config);
162
+ const hits = await adapter.query(vector, topK, query);
163
+
164
+ const result: DemoResult = {
165
+ query,
166
+ hits,
167
+ source: "direct",
168
+ tookMs: Date.now() - started,
169
+ };
170
+ return NextResponse.json(result);
171
+ } catch (err) {
172
+ const message =
173
+ err instanceof Error ? err.message : "Retrieval failed unexpectedly.";
174
+ return NextResponse.json({ error: message }, { status: 500 });
175
+ }
176
+ }