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,714 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useRef, useState } from "react";
|
|
4
|
+
import { toast } from "sonner";
|
|
5
|
+
import { formatErrorMessage } from "@/lib/error-formatter";
|
|
6
|
+
import { FileUp, Loader2, X, Settings2, Columns, Plus } from "lucide-react";
|
|
7
|
+
import { Button } from "@/components/ui/button";
|
|
8
|
+
import { Input } from "@/components/ui/input";
|
|
9
|
+
import { cn } from "@/lib/utils";
|
|
10
|
+
import Papa from "papaparse";
|
|
11
|
+
import * as XLSX from "xlsx";
|
|
12
|
+
import {
|
|
13
|
+
Dialog,
|
|
14
|
+
DialogContent,
|
|
15
|
+
DialogHeader,
|
|
16
|
+
DialogTitle,
|
|
17
|
+
DialogFooter,
|
|
18
|
+
} from "@/components/ui/dialog";
|
|
19
|
+
import {
|
|
20
|
+
Select,
|
|
21
|
+
SelectContent,
|
|
22
|
+
SelectItem,
|
|
23
|
+
SelectTrigger,
|
|
24
|
+
SelectValue,
|
|
25
|
+
} from "@/components/ui/select";
|
|
26
|
+
import { Checkbox } from "@/components/ui/checkbox";
|
|
27
|
+
import { Label } from "@/components/ui/label";
|
|
28
|
+
import { Switch } from "@/components/ui/switch";
|
|
29
|
+
import { Progress } from "@/components/ui/progress";
|
|
30
|
+
|
|
31
|
+
const ACCEPT = ".txt,.md,.markdown,.json,.csv,.html,.htm,.log,.xlsx,.xls,.pdf,.doc,.docx";
|
|
32
|
+
|
|
33
|
+
type FileFormat = "plain" | "lines" | "structured";
|
|
34
|
+
|
|
35
|
+
interface StagedFile {
|
|
36
|
+
id: string;
|
|
37
|
+
name: string;
|
|
38
|
+
size: number;
|
|
39
|
+
format: FileFormat;
|
|
40
|
+
rawContent?: string;
|
|
41
|
+
rows?: any[];
|
|
42
|
+
keys?: string[];
|
|
43
|
+
// mapping for structured
|
|
44
|
+
titleKey?: string;
|
|
45
|
+
contentKeys?: string[];
|
|
46
|
+
contentSeparator?: string;
|
|
47
|
+
metadataKeys?: string[];
|
|
48
|
+
globalMetadata?: { key: string; value: string }[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function UploadPanel({ onAdded }: { onAdded: () => void }) {
|
|
52
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
53
|
+
const [staged, setStaged] = useState<StagedFile[]>([]);
|
|
54
|
+
const [dragging, setDragging] = useState(false);
|
|
55
|
+
const [saving, setSaving] = useState(false);
|
|
56
|
+
const [progress, setProgress] = useState<{
|
|
57
|
+
current: number;
|
|
58
|
+
total: number;
|
|
59
|
+
} | null>(null);
|
|
60
|
+
|
|
61
|
+
// Mapping state
|
|
62
|
+
const [editingFileId, setEditingFileId] = useState<string | null>(null);
|
|
63
|
+
const editingFile = staged.find((f) => f.id === editingFileId);
|
|
64
|
+
|
|
65
|
+
async function readFiles(files: FileList | File[]) {
|
|
66
|
+
const next: StagedFile[] = [];
|
|
67
|
+
for (const file of Array.from(files)) {
|
|
68
|
+
const ext = file.name.split(".").pop()?.toLowerCase();
|
|
69
|
+
const id = Math.random().toString(36).slice(2);
|
|
70
|
+
try {
|
|
71
|
+
if (ext === "csv") {
|
|
72
|
+
const content = await file.text();
|
|
73
|
+
const result = Papa.parse(content, {
|
|
74
|
+
header: true,
|
|
75
|
+
skipEmptyLines: true,
|
|
76
|
+
});
|
|
77
|
+
if (result.data.length > 0) {
|
|
78
|
+
const keys = Object.keys(result.data[0] as object);
|
|
79
|
+
next.push({
|
|
80
|
+
id,
|
|
81
|
+
name: file.name,
|
|
82
|
+
size: file.size,
|
|
83
|
+
format: "structured",
|
|
84
|
+
rows: result.data,
|
|
85
|
+
keys,
|
|
86
|
+
titleKey: keys[0],
|
|
87
|
+
contentKeys: keys,
|
|
88
|
+
contentSeparator: ", ",
|
|
89
|
+
metadataKeys: [],
|
|
90
|
+
globalMetadata: [],
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
} else if (ext === "xlsx" || ext === "xls") {
|
|
94
|
+
const data = await file.arrayBuffer();
|
|
95
|
+
const workbook = XLSX.read(data, { type: "array" });
|
|
96
|
+
const sheet = workbook.Sheets[workbook.SheetNames[0]];
|
|
97
|
+
const rows = XLSX.utils.sheet_to_json(sheet);
|
|
98
|
+
if (rows.length > 0) {
|
|
99
|
+
const keys = Object.keys(rows[0] as object);
|
|
100
|
+
next.push({
|
|
101
|
+
id,
|
|
102
|
+
name: file.name,
|
|
103
|
+
size: file.size,
|
|
104
|
+
format: "structured",
|
|
105
|
+
rows,
|
|
106
|
+
keys,
|
|
107
|
+
titleKey: keys[0],
|
|
108
|
+
contentKeys: keys,
|
|
109
|
+
contentSeparator: ", ",
|
|
110
|
+
metadataKeys: [],
|
|
111
|
+
globalMetadata: [],
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
} else if (ext === "json") {
|
|
115
|
+
const content = await file.text();
|
|
116
|
+
try {
|
|
117
|
+
const parsed = JSON.parse(content);
|
|
118
|
+
if (
|
|
119
|
+
Array.isArray(parsed) &&
|
|
120
|
+
parsed.length > 0 &&
|
|
121
|
+
typeof parsed[0] === "object"
|
|
122
|
+
) {
|
|
123
|
+
const keys = Object.keys(parsed[0]);
|
|
124
|
+
next.push({
|
|
125
|
+
id,
|
|
126
|
+
name: file.name,
|
|
127
|
+
size: file.size,
|
|
128
|
+
format: "structured",
|
|
129
|
+
rows: parsed,
|
|
130
|
+
keys,
|
|
131
|
+
titleKey: keys[0],
|
|
132
|
+
contentKeys: keys,
|
|
133
|
+
contentSeparator: ", ",
|
|
134
|
+
metadataKeys: [],
|
|
135
|
+
globalMetadata: [],
|
|
136
|
+
});
|
|
137
|
+
} else {
|
|
138
|
+
// fallback to plain
|
|
139
|
+
next.push({
|
|
140
|
+
id,
|
|
141
|
+
name: file.name,
|
|
142
|
+
size: file.size,
|
|
143
|
+
format: "plain",
|
|
144
|
+
rawContent: content,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
} catch {
|
|
148
|
+
next.push({
|
|
149
|
+
id,
|
|
150
|
+
name: file.name,
|
|
151
|
+
size: file.size,
|
|
152
|
+
format: "plain",
|
|
153
|
+
rawContent: content,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
} else if (ext === "pdf" || ext === "doc" || ext === "docx") {
|
|
157
|
+
const formData = new FormData();
|
|
158
|
+
formData.append("file", file);
|
|
159
|
+
|
|
160
|
+
const res = await fetch("/api/parse-file", {
|
|
161
|
+
method: "POST",
|
|
162
|
+
body: formData,
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
if (!res.ok) {
|
|
166
|
+
throw new Error(`Failed to parse ${file.name}`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const { text } = await res.json();
|
|
170
|
+
next.push({
|
|
171
|
+
id,
|
|
172
|
+
name: file.name,
|
|
173
|
+
size: file.size,
|
|
174
|
+
format: "plain",
|
|
175
|
+
rawContent: text,
|
|
176
|
+
});
|
|
177
|
+
} else {
|
|
178
|
+
const content = await file.text();
|
|
179
|
+
next.push({
|
|
180
|
+
id,
|
|
181
|
+
name: file.name,
|
|
182
|
+
size: file.size,
|
|
183
|
+
format: "plain",
|
|
184
|
+
rawContent: content,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
} catch (err) {
|
|
188
|
+
toast.error(`Could not read ${file.name}. ${formatErrorMessage(err)}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
setStaged((prev) => [...prev, ...next]);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function ingest() {
|
|
195
|
+
if (staged.length === 0) return;
|
|
196
|
+
setSaving(true);
|
|
197
|
+
let ok = 0;
|
|
198
|
+
|
|
199
|
+
const payloads: any[] = [];
|
|
200
|
+
|
|
201
|
+
for (const f of staged) {
|
|
202
|
+
if (f.format === "plain" && f.rawContent) {
|
|
203
|
+
payloads.push({
|
|
204
|
+
title: f.name,
|
|
205
|
+
content: f.rawContent,
|
|
206
|
+
source: "upload",
|
|
207
|
+
});
|
|
208
|
+
} else if (f.format === "lines" && f.rawContent) {
|
|
209
|
+
const lines = f.rawContent
|
|
210
|
+
.split(/\r?\n/)
|
|
211
|
+
.map((l) => l.trim())
|
|
212
|
+
.filter(Boolean);
|
|
213
|
+
lines.forEach((line, i) => {
|
|
214
|
+
payloads.push({
|
|
215
|
+
title: `${f.name} - Line ${i + 1}`,
|
|
216
|
+
content: line,
|
|
217
|
+
source: "upload",
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
} else if (f.format === "structured" && f.rows) {
|
|
221
|
+
for (let i = 0; i < f.rows.length; i++) {
|
|
222
|
+
const row = f.rows[i];
|
|
223
|
+
const title = f.titleKey
|
|
224
|
+
? String(row[f.titleKey] || `Row ${i + 1}`)
|
|
225
|
+
: `Row ${i + 1}`;
|
|
226
|
+
|
|
227
|
+
let content = "";
|
|
228
|
+
if (f.contentKeys && f.contentKeys.length > 0) {
|
|
229
|
+
content = f.contentKeys
|
|
230
|
+
.map((k) => String(row[k] || ""))
|
|
231
|
+
.filter(Boolean)
|
|
232
|
+
.join(f.contentSeparator || " ");
|
|
233
|
+
} else {
|
|
234
|
+
content = JSON.stringify(row);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const metadata: Record<string, any> = {};
|
|
238
|
+
if (f.metadataKeys) {
|
|
239
|
+
for (const mk of f.metadataKeys) {
|
|
240
|
+
metadata[mk] = row[mk];
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (f.globalMetadata) {
|
|
244
|
+
for (const gm of f.globalMetadata) {
|
|
245
|
+
if (gm.key.trim()) {
|
|
246
|
+
metadata[gm.key.trim()] = gm.value;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (content.trim()) {
|
|
252
|
+
payloads.push({ title, content, metadata, source: "upload" });
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
setProgress({ current: 0, total: payloads.length });
|
|
259
|
+
|
|
260
|
+
for (const [index, p] of payloads.entries()) {
|
|
261
|
+
try {
|
|
262
|
+
const res = await fetch("/api/documents", {
|
|
263
|
+
method: "POST",
|
|
264
|
+
headers: { "Content-Type": "application/json" },
|
|
265
|
+
body: JSON.stringify(p),
|
|
266
|
+
});
|
|
267
|
+
if (res.ok) ok++;
|
|
268
|
+
} catch {
|
|
269
|
+
/* continue */
|
|
270
|
+
}
|
|
271
|
+
setProgress({ current: index + 1, total: payloads.length });
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
setSaving(false);
|
|
275
|
+
setProgress(null);
|
|
276
|
+
setStaged([]);
|
|
277
|
+
if (ok > 0) {
|
|
278
|
+
toast.success(
|
|
279
|
+
`Added ${ok} document${ok > 1 ? "s" : ""} from ${staged.length} file${staged.length > 1 ? "s" : ""}`,
|
|
280
|
+
);
|
|
281
|
+
onAdded();
|
|
282
|
+
} else {
|
|
283
|
+
toast.error("No documents could be added.");
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function updateEditingFile(patch: Partial<StagedFile>) {
|
|
288
|
+
setStaged((prev) =>
|
|
289
|
+
prev.map((f) => (f.id === editingFileId ? { ...f, ...patch } : f)),
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return (
|
|
294
|
+
<div className="space-y-4 cursor-pointer">
|
|
295
|
+
<button
|
|
296
|
+
type="button"
|
|
297
|
+
onClick={() => inputRef.current?.click()}
|
|
298
|
+
onDragOver={(e) => {
|
|
299
|
+
e.preventDefault();
|
|
300
|
+
setDragging(true);
|
|
301
|
+
}}
|
|
302
|
+
onDragLeave={() => setDragging(false)}
|
|
303
|
+
onDrop={(e) => {
|
|
304
|
+
e.preventDefault();
|
|
305
|
+
setDragging(false);
|
|
306
|
+
if (e.dataTransfer.files?.length) readFiles(e.dataTransfer.files);
|
|
307
|
+
}}
|
|
308
|
+
className={cn(
|
|
309
|
+
"flex w-full flex-col cursor-pointer items-center justify-center gap-2 rounded-lg border border-dashed border-border bg-muted/40 px-6 py-10 text-center transition-colors hover:bg-muted/70",
|
|
310
|
+
dragging && "border-primary bg-accent",
|
|
311
|
+
)}
|
|
312
|
+
>
|
|
313
|
+
<FileUp className="size-6 text-muted-foreground" />
|
|
314
|
+
<span className="text-sm font-medium">
|
|
315
|
+
Drop files here or click to browse
|
|
316
|
+
</span>
|
|
317
|
+
<span className="text-xs text-muted-foreground">
|
|
318
|
+
Text, JSON, CSV, Excel, PDF, and Word files
|
|
319
|
+
</span>
|
|
320
|
+
</button>
|
|
321
|
+
<input
|
|
322
|
+
ref={inputRef}
|
|
323
|
+
type="file"
|
|
324
|
+
multiple
|
|
325
|
+
accept={ACCEPT}
|
|
326
|
+
className="sr-only"
|
|
327
|
+
onChange={(e) => {
|
|
328
|
+
if (e.target.files?.length) readFiles(e.target.files);
|
|
329
|
+
e.target.value = "";
|
|
330
|
+
}}
|
|
331
|
+
/>
|
|
332
|
+
|
|
333
|
+
{staged.length > 0 && (
|
|
334
|
+
<ul className="space-y-1.5">
|
|
335
|
+
{staged.map((f) => (
|
|
336
|
+
<li
|
|
337
|
+
key={f.id}
|
|
338
|
+
className="flex items-center justify-between gap-3 rounded-md border border-border bg-card px-3 py-2 text-sm"
|
|
339
|
+
>
|
|
340
|
+
<div className="min-w-0 flex-1 flex flex-col gap-0.5">
|
|
341
|
+
<span className="truncate font-mono text-xs">{f.name}</span>
|
|
342
|
+
<span className="text-[10px] text-muted-foreground uppercase tracking-wider">
|
|
343
|
+
{f.format === "structured"
|
|
344
|
+
? "STRUCTURED"
|
|
345
|
+
: f.format === "lines"
|
|
346
|
+
? "SPLIT BY LINE"
|
|
347
|
+
: "PLAIN TEXT"}
|
|
348
|
+
{f.format === "structured" && ` • ${f.rows?.length} ROWS`}
|
|
349
|
+
</span>
|
|
350
|
+
</div>
|
|
351
|
+
<span className="shrink-0 text-xs text-muted-foreground tabular-nums">
|
|
352
|
+
{(f.size / 1024).toFixed(1)} KB
|
|
353
|
+
</span>
|
|
354
|
+
|
|
355
|
+
<div className="flex items-center gap-1 shrink-0">
|
|
356
|
+
<button
|
|
357
|
+
type="button"
|
|
358
|
+
aria-label={`Configure ${f.name}`}
|
|
359
|
+
onClick={() => setEditingFileId(f.id)}
|
|
360
|
+
className="p-1.5 bg-secondary cursor-pointer border text-muted-foreground hover:bg-muted/50 hover:text-foreground rounded-md transition-colors"
|
|
361
|
+
>
|
|
362
|
+
<Settings2 className="size-3.5" />
|
|
363
|
+
</button>
|
|
364
|
+
<button
|
|
365
|
+
type="button"
|
|
366
|
+
aria-label={`Remove ${f.name}`}
|
|
367
|
+
onClick={() =>
|
|
368
|
+
setStaged((p) => p.filter((item) => item.id !== f.id))
|
|
369
|
+
}
|
|
370
|
+
className="p-1.5 border bg-muted hover:bg-muted/50 cursor-pointer text-muted-foreground hover:text-foreground rounded-md transition-colors"
|
|
371
|
+
>
|
|
372
|
+
<X className="size-3.5 text-red-500" />
|
|
373
|
+
</button>
|
|
374
|
+
</div>
|
|
375
|
+
</li>
|
|
376
|
+
))}
|
|
377
|
+
</ul>
|
|
378
|
+
)}
|
|
379
|
+
|
|
380
|
+
{progress && (
|
|
381
|
+
<div className="space-y-2">
|
|
382
|
+
<div className="flex items-center justify-between text-xs text-muted-foreground">
|
|
383
|
+
<span>Uploading documents...</span>
|
|
384
|
+
<span className="tabular-nums font-mono">
|
|
385
|
+
{progress.current} / {progress.total}
|
|
386
|
+
</span>
|
|
387
|
+
</div>
|
|
388
|
+
<Progress value={(progress.current / progress.total) * 100} />
|
|
389
|
+
</div>
|
|
390
|
+
)}
|
|
391
|
+
|
|
392
|
+
<Button onClick={ingest} disabled={saving || staged.length === 0}>
|
|
393
|
+
{saving ? (
|
|
394
|
+
<Loader2 className="size-4 animate-spin" />
|
|
395
|
+
) : (
|
|
396
|
+
<FileUp className="size-4" />
|
|
397
|
+
)}
|
|
398
|
+
{saving && progress
|
|
399
|
+
? `Adding ${progress.current} of ${progress.total}`
|
|
400
|
+
: `Add ${staged.length > 0 ? staged.length : ""} file${staged.length === 1 ? "" : "s"} to corpus`}
|
|
401
|
+
</Button>
|
|
402
|
+
|
|
403
|
+
{/* Mapping Dialog */}
|
|
404
|
+
<Dialog
|
|
405
|
+
open={!!editingFile}
|
|
406
|
+
onOpenChange={(open) => !open && setEditingFileId(null)}
|
|
407
|
+
>
|
|
408
|
+
<DialogContent className="sm:max-w-xl max-h-[80%]">
|
|
409
|
+
<DialogHeader>
|
|
410
|
+
<DialogTitle className="flex items-center gap-2">
|
|
411
|
+
<Columns className="size-4 text-primary" />
|
|
412
|
+
Configure Ingestion
|
|
413
|
+
</DialogTitle>
|
|
414
|
+
</DialogHeader>
|
|
415
|
+
|
|
416
|
+
{editingFile && (
|
|
417
|
+
<div className="py-4 space-y-6 overflow-y-auto pr-2" style={{ maxHeight: "calc(80vh - 120px)" }}>
|
|
418
|
+
{editingFile.format === "structured" ? (
|
|
419
|
+
<>
|
|
420
|
+
<div className="space-y-4">
|
|
421
|
+
<div className="space-y-2">
|
|
422
|
+
<Label>Title Column</Label>
|
|
423
|
+
<Select
|
|
424
|
+
value={editingFile.titleKey}
|
|
425
|
+
onValueChange={(val) =>
|
|
426
|
+
updateEditingFile({ titleKey: val || undefined })
|
|
427
|
+
}
|
|
428
|
+
>
|
|
429
|
+
<SelectTrigger>
|
|
430
|
+
<SelectValue placeholder="Select column" />
|
|
431
|
+
</SelectTrigger>
|
|
432
|
+
<SelectContent>
|
|
433
|
+
{editingFile.keys?.map((k) => (
|
|
434
|
+
<SelectItem key={k} value={k}>
|
|
435
|
+
{k}
|
|
436
|
+
</SelectItem>
|
|
437
|
+
))}
|
|
438
|
+
</SelectContent>
|
|
439
|
+
</Select>
|
|
440
|
+
<p className="text-[11px] text-muted-foreground">
|
|
441
|
+
Used as the document name.
|
|
442
|
+
</p>
|
|
443
|
+
</div>
|
|
444
|
+
|
|
445
|
+
<div className="space-y-3 pt-2 border-t border-border">
|
|
446
|
+
<div className="flex items-center justify-between">
|
|
447
|
+
<Label>Content Columns</Label>
|
|
448
|
+
<div className="flex items-center gap-2">
|
|
449
|
+
<button
|
|
450
|
+
type="button"
|
|
451
|
+
className="text-[10px] uppercase font-medium text-primary hover:underline cursor-pointer"
|
|
452
|
+
onClick={() => {
|
|
453
|
+
updateEditingFile({
|
|
454
|
+
contentKeys: editingFile.keys || []
|
|
455
|
+
});
|
|
456
|
+
}}
|
|
457
|
+
>
|
|
458
|
+
Select All
|
|
459
|
+
</button>
|
|
460
|
+
<span className="text-muted-foreground text-[10px]">|</span>
|
|
461
|
+
<button
|
|
462
|
+
type="button"
|
|
463
|
+
className="text-[10px] uppercase font-medium text-muted-foreground hover:underline cursor-pointer"
|
|
464
|
+
onClick={() => {
|
|
465
|
+
updateEditingFile({ contentKeys: [] });
|
|
466
|
+
}}
|
|
467
|
+
>
|
|
468
|
+
Clear
|
|
469
|
+
</button>
|
|
470
|
+
</div>
|
|
471
|
+
</div>
|
|
472
|
+
<p className="text-[11px] text-muted-foreground mb-2 -mt-1">
|
|
473
|
+
Selected columns will be combined to form the searchable
|
|
474
|
+
content.
|
|
475
|
+
</p>
|
|
476
|
+
<div className="max-h-[120px] overflow-y-auto space-y-2 pr-2">
|
|
477
|
+
{editingFile.keys?.map((k) => {
|
|
478
|
+
const isSelected =
|
|
479
|
+
editingFile.contentKeys?.includes(k);
|
|
480
|
+
return (
|
|
481
|
+
<div
|
|
482
|
+
key={k}
|
|
483
|
+
className="flex items-center space-x-2"
|
|
484
|
+
>
|
|
485
|
+
<Checkbox
|
|
486
|
+
id={`content-${k}`}
|
|
487
|
+
checked={isSelected}
|
|
488
|
+
onCheckedChange={(checked) => {
|
|
489
|
+
const current = editingFile.contentKeys || [];
|
|
490
|
+
if (checked) {
|
|
491
|
+
updateEditingFile({
|
|
492
|
+
contentKeys: [...current, k],
|
|
493
|
+
});
|
|
494
|
+
} else {
|
|
495
|
+
updateEditingFile({
|
|
496
|
+
contentKeys: current.filter(
|
|
497
|
+
(x) => x !== k,
|
|
498
|
+
),
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
}}
|
|
502
|
+
/>
|
|
503
|
+
<label
|
|
504
|
+
htmlFor={`content-${k}`}
|
|
505
|
+
className="text-sm font-medium leading-none"
|
|
506
|
+
>
|
|
507
|
+
{k}
|
|
508
|
+
</label>
|
|
509
|
+
</div>
|
|
510
|
+
);
|
|
511
|
+
})}
|
|
512
|
+
</div>
|
|
513
|
+
|
|
514
|
+
{editingFile.contentKeys &&
|
|
515
|
+
editingFile.contentKeys.length > 1 && (
|
|
516
|
+
<div className="pt-2">
|
|
517
|
+
<Label className="mb-2 block">Separator</Label>
|
|
518
|
+
<Select
|
|
519
|
+
value={editingFile.contentSeparator || " "}
|
|
520
|
+
onValueChange={(val) =>
|
|
521
|
+
updateEditingFile({
|
|
522
|
+
contentSeparator: val || undefined,
|
|
523
|
+
})
|
|
524
|
+
}
|
|
525
|
+
>
|
|
526
|
+
<SelectTrigger>
|
|
527
|
+
<SelectValue placeholder="Select separator" />
|
|
528
|
+
</SelectTrigger>
|
|
529
|
+
<SelectContent>
|
|
530
|
+
<SelectItem value=" ">Space</SelectItem>
|
|
531
|
+
<SelectItem value=", ">Comma</SelectItem>
|
|
532
|
+
<SelectItem value="\n">Newline</SelectItem>
|
|
533
|
+
<SelectItem value=" - ">Hyphen</SelectItem>
|
|
534
|
+
</SelectContent>
|
|
535
|
+
</Select>
|
|
536
|
+
</div>
|
|
537
|
+
)}
|
|
538
|
+
</div>
|
|
539
|
+
|
|
540
|
+
<div className="space-y-3 pt-2 border-t border-border">
|
|
541
|
+
<div className="flex items-center justify-between">
|
|
542
|
+
<Label>Metadata Columns</Label>
|
|
543
|
+
<div className="flex items-center gap-2">
|
|
544
|
+
<button
|
|
545
|
+
type="button"
|
|
546
|
+
className="text-[10px] uppercase font-medium text-primary hover:underline cursor-pointer"
|
|
547
|
+
onClick={() => {
|
|
548
|
+
const availableKeys = editingFile.keys?.filter(k => k !== editingFile.titleKey) || [];
|
|
549
|
+
updateEditingFile({
|
|
550
|
+
metadataKeys: availableKeys
|
|
551
|
+
});
|
|
552
|
+
}}
|
|
553
|
+
>
|
|
554
|
+
Select All
|
|
555
|
+
</button>
|
|
556
|
+
<span className="text-muted-foreground text-[10px]">|</span>
|
|
557
|
+
<button
|
|
558
|
+
type="button"
|
|
559
|
+
className="text-[10px] uppercase font-medium text-muted-foreground hover:underline cursor-pointer"
|
|
560
|
+
onClick={() => {
|
|
561
|
+
updateEditingFile({ metadataKeys: [] });
|
|
562
|
+
}}
|
|
563
|
+
>
|
|
564
|
+
Clear
|
|
565
|
+
</button>
|
|
566
|
+
</div>
|
|
567
|
+
</div>
|
|
568
|
+
<p className="text-[11px] text-muted-foreground mb-2 -mt-1">
|
|
569
|
+
These columns will be stored as searchable metadata.
|
|
570
|
+
</p>
|
|
571
|
+
<div className="max-h-[120px] overflow-y-auto space-y-2 pr-2">
|
|
572
|
+
{editingFile.keys?.map((k) => {
|
|
573
|
+
const isSelected =
|
|
574
|
+
editingFile.metadataKeys?.includes(k);
|
|
575
|
+
const disabled = k === editingFile.titleKey;
|
|
576
|
+
return (
|
|
577
|
+
<div
|
|
578
|
+
key={k}
|
|
579
|
+
className="flex items-center space-x-2"
|
|
580
|
+
>
|
|
581
|
+
<Checkbox
|
|
582
|
+
id={`meta-${k}`}
|
|
583
|
+
checked={isSelected}
|
|
584
|
+
disabled={disabled}
|
|
585
|
+
onCheckedChange={(checked) => {
|
|
586
|
+
const current =
|
|
587
|
+
editingFile.metadataKeys || [];
|
|
588
|
+
if (checked) {
|
|
589
|
+
updateEditingFile({
|
|
590
|
+
metadataKeys: [...current, k],
|
|
591
|
+
});
|
|
592
|
+
} else {
|
|
593
|
+
updateEditingFile({
|
|
594
|
+
metadataKeys: current.filter(
|
|
595
|
+
(x) => x !== k,
|
|
596
|
+
),
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
}}
|
|
600
|
+
/>
|
|
601
|
+
<label
|
|
602
|
+
htmlFor={`meta-${k}`}
|
|
603
|
+
className={cn(
|
|
604
|
+
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
|
|
605
|
+
disabled && "text-muted-foreground",
|
|
606
|
+
)}
|
|
607
|
+
>
|
|
608
|
+
{k} {disabled && "(used)"}
|
|
609
|
+
</label>
|
|
610
|
+
</div>
|
|
611
|
+
);
|
|
612
|
+
})}
|
|
613
|
+
</div>
|
|
614
|
+
</div>
|
|
615
|
+
</div>
|
|
616
|
+
</>
|
|
617
|
+
) : (
|
|
618
|
+
<div className="space-y-4">
|
|
619
|
+
<div className="flex items-center justify-between rounded-lg border border-border p-3">
|
|
620
|
+
<div className="space-y-0.5">
|
|
621
|
+
<Label>Split by Line</Label>
|
|
622
|
+
<p className="text-[11px] text-muted-foreground">
|
|
623
|
+
Treat each line as a completely separate document.
|
|
624
|
+
</p>
|
|
625
|
+
</div>
|
|
626
|
+
<Switch
|
|
627
|
+
checked={editingFile.format === "lines"}
|
|
628
|
+
onCheckedChange={(checked) =>
|
|
629
|
+
updateEditingFile({
|
|
630
|
+
format: checked ? "lines" : "plain",
|
|
631
|
+
})
|
|
632
|
+
}
|
|
633
|
+
/>
|
|
634
|
+
</div>
|
|
635
|
+
</div>
|
|
636
|
+
)}
|
|
637
|
+
|
|
638
|
+
<div className="space-y-3 pt-4 border-t border-border">
|
|
639
|
+
<div className="flex items-center justify-between">
|
|
640
|
+
<Label>Global Custom Metadata</Label>
|
|
641
|
+
<Button
|
|
642
|
+
variant="ghost"
|
|
643
|
+
size="sm"
|
|
644
|
+
className="h-6 px-2 text-xs"
|
|
645
|
+
onClick={() => {
|
|
646
|
+
const current = editingFile.globalMetadata || [];
|
|
647
|
+
updateEditingFile({
|
|
648
|
+
globalMetadata: [...current, { key: "", value: "" }],
|
|
649
|
+
});
|
|
650
|
+
}}
|
|
651
|
+
>
|
|
652
|
+
<Plus className="size-3 mr-1" /> Add Field
|
|
653
|
+
</Button>
|
|
654
|
+
</div>
|
|
655
|
+
<p className="text-[11px] text-muted-foreground mb-2">
|
|
656
|
+
Apply custom key-value pairs to every document generated from
|
|
657
|
+
this file.
|
|
658
|
+
</p>
|
|
659
|
+
<div className="space-y-2">
|
|
660
|
+
{(editingFile.globalMetadata || []).map((gm, i) => (
|
|
661
|
+
<div key={i} className="flex items-center gap-2">
|
|
662
|
+
<Input
|
|
663
|
+
placeholder="Key"
|
|
664
|
+
value={gm.key}
|
|
665
|
+
className="h-8 text-xs font-mono"
|
|
666
|
+
onChange={(e) => {
|
|
667
|
+
const next = [...(editingFile.globalMetadata || [])];
|
|
668
|
+
next[i].key = e.target.value;
|
|
669
|
+
updateEditingFile({ globalMetadata: next });
|
|
670
|
+
}}
|
|
671
|
+
/>
|
|
672
|
+
<Input
|
|
673
|
+
placeholder="Value"
|
|
674
|
+
value={gm.value}
|
|
675
|
+
className="h-8 text-xs font-mono"
|
|
676
|
+
onChange={(e) => {
|
|
677
|
+
const next = [...(editingFile.globalMetadata || [])];
|
|
678
|
+
next[i].value = e.target.value;
|
|
679
|
+
updateEditingFile({ globalMetadata: next });
|
|
680
|
+
}}
|
|
681
|
+
/>
|
|
682
|
+
<Button
|
|
683
|
+
variant="ghost"
|
|
684
|
+
size="icon"
|
|
685
|
+
className="h-8 w-8 text-muted-foreground shrink-0"
|
|
686
|
+
onClick={() => {
|
|
687
|
+
const next = [...(editingFile.globalMetadata || [])];
|
|
688
|
+
next.splice(i, 1);
|
|
689
|
+
updateEditingFile({ globalMetadata: next });
|
|
690
|
+
}}
|
|
691
|
+
>
|
|
692
|
+
<X className="size-3.5" />
|
|
693
|
+
</Button>
|
|
694
|
+
</div>
|
|
695
|
+
))}
|
|
696
|
+
{(!editingFile.globalMetadata ||
|
|
697
|
+
editingFile.globalMetadata.length === 0) && (
|
|
698
|
+
<p className="text-xs text-muted-foreground italic text-center py-2">
|
|
699
|
+
No custom metadata.
|
|
700
|
+
</p>
|
|
701
|
+
)}
|
|
702
|
+
</div>
|
|
703
|
+
</div>
|
|
704
|
+
</div>
|
|
705
|
+
)}
|
|
706
|
+
|
|
707
|
+
<DialogFooter>
|
|
708
|
+
<Button onClick={() => setEditingFileId(null)}>Done</Button>
|
|
709
|
+
</DialogFooter>
|
|
710
|
+
</DialogContent>
|
|
711
|
+
</Dialog>
|
|
712
|
+
</div>
|
|
713
|
+
);
|
|
714
|
+
}
|