@larkup/workspace-app 0.2.1 → 0.2.2
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.
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"corpus-panel.d.ts","sourceRoot":"","sources":["../../../src/components/data/corpus-panel.tsx"],"names":[],"mappings":"AAqCA,OAAO,KAAK,EAAkB,cAAc,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AA+BpF,OAAO,EAGL,KAAK,iBAAiB,EACvB,MAAM,qCAAqC,CAAC;AAU7C,wBAAgB,WAAW,CAAC,EAC1B,SAAS,EACT,MAAW,EACX,SAAS,EACT,UAAU,EACV,uBAAuB,EACvB,cAAc,EACd,SAAS,GACV,EAAE;IACD,SAAS,EAAE,cAAc,EAAE,CAAC;IAC5B,MAAM,CAAC,EAAE,SAAS,EAAE,CAAC;IACrB,SAAS,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACzC,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,uBAAuB,EAAE,iBAAiB,CAAC;IAC3C,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB,+
|
|
1
|
+
{"version":3,"file":"corpus-panel.d.ts","sourceRoot":"","sources":["../../../src/components/data/corpus-panel.tsx"],"names":[],"mappings":"AAqCA,OAAO,KAAK,EAAkB,cAAc,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AA+BpF,OAAO,EAGL,KAAK,iBAAiB,EACvB,MAAM,qCAAqC,CAAC;AAU7C,wBAAgB,WAAW,CAAC,EAC1B,SAAS,EACT,MAAW,EACX,SAAS,EACT,UAAU,EACV,uBAAuB,EACvB,cAAc,EACd,SAAS,GACV,EAAE;IACD,SAAS,EAAE,cAAc,EAAE,CAAC;IAC5B,MAAM,CAAC,EAAE,SAAS,EAAE,CAAC;IACrB,SAAS,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACzC,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,uBAAuB,EAAE,iBAAiB,CAAC;IAC3C,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB,+BAquBA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"upload-panel.d.ts","sourceRoot":"","sources":["../../../src/components/data/upload-panel.tsx"],"names":[],"mappings":"AA8BA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"upload-panel.d.ts","sourceRoot":"","sources":["../../../src/components/data/upload-panel.tsx"],"names":[],"mappings":"AA8BA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAiF/D,wBAAgB,WAAW,CAAC,EAC1B,OAAO,EACP,cAAc,EACd,OAAO,GACR,EAAE;IACD,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,iBAAiB,GAAG,IAAI,KAAK,IAAI,CAAC;IAC5D,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,+BAuuCA"}
|
package/package.json
CHANGED
|
@@ -102,6 +102,8 @@ export function CorpusPanel({
|
|
|
102
102
|
{ type: 'single'; doc: SourceDocument } | { type: 'all' } | { type: 'selected' } | null
|
|
103
103
|
>(null);
|
|
104
104
|
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
|
|
105
|
+
const [deletedIds, setDeletedIds] = useState<Set<string>>(new Set());
|
|
106
|
+
const [clearingAll, setClearingAll] = useState(false);
|
|
105
107
|
const [movingGroupId, setMovingGroupId] = useState<string | null>(null);
|
|
106
108
|
const [inspectorDoc, setInspectorDoc] = useState<SourceDocument | null>(null);
|
|
107
109
|
|
|
@@ -143,9 +145,18 @@ export function CorpusPanel({
|
|
|
143
145
|
others.push(doc);
|
|
144
146
|
}
|
|
145
147
|
}
|
|
146
|
-
return [...Array.from(grouped.values()), ...others]
|
|
147
|
-
(
|
|
148
|
-
|
|
148
|
+
return [...Array.from(grouped.values()), ...others]
|
|
149
|
+
.filter((document) => !deletedIds.has(document.id))
|
|
150
|
+
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
|
151
|
+
}, [deletedIds, documents]);
|
|
152
|
+
|
|
153
|
+
useEffect(() => {
|
|
154
|
+
setDeletedIds((current) => {
|
|
155
|
+
const documentIds = new Set(documents.map((document) => document.id));
|
|
156
|
+
const pending = new Set(Array.from(current).filter((id) => documentIds.has(id)));
|
|
157
|
+
return pending.size === current.size ? current : pending;
|
|
158
|
+
});
|
|
159
|
+
if (documents.length === 0) setClearingAll(false);
|
|
149
160
|
}, [documents]);
|
|
150
161
|
|
|
151
162
|
const filteredDocuments = groupedDocuments.filter(
|
|
@@ -182,6 +193,8 @@ export function CorpusPanel({
|
|
|
182
193
|
}
|
|
183
194
|
|
|
184
195
|
async function del(id: string, doc?: SourceDocument) {
|
|
196
|
+
setDeleteTask(null);
|
|
197
|
+
setDeletedIds((current) => new Set(current).add(id));
|
|
185
198
|
try {
|
|
186
199
|
if (doc?.metadata?.isGroup && doc?.metadata?.mediaAssetId) {
|
|
187
200
|
await deleteRequest(workspaceUrl(`/api/media?id=${doc.metadata.mediaAssetId}`));
|
|
@@ -196,12 +209,20 @@ export function CorpusPanel({
|
|
|
196
209
|
await onChanged();
|
|
197
210
|
toast.success('Document deleted');
|
|
198
211
|
} catch (error) {
|
|
212
|
+
setDeletedIds((current) => {
|
|
213
|
+
const next = new Set(current);
|
|
214
|
+
next.delete(id);
|
|
215
|
+
return next;
|
|
216
|
+
});
|
|
199
217
|
toast.error(formatErrorMessage(error) || 'Could not delete this document.');
|
|
200
218
|
}
|
|
201
219
|
}
|
|
202
220
|
async function delSelected() {
|
|
203
221
|
if (selectedIds.size === 0) return;
|
|
204
222
|
const toDelete = groupedDocuments.filter((d) => selectedIds.has(d.id));
|
|
223
|
+
const deletionIds = new Set(toDelete.map((document) => document.id));
|
|
224
|
+
setDeleteTask(null);
|
|
225
|
+
setDeletedIds((current) => new Set([...current, ...deletionIds]));
|
|
205
226
|
try {
|
|
206
227
|
for (const doc of toDelete) {
|
|
207
228
|
if (doc.metadata?.isGroup && doc.metadata?.mediaAssetId) {
|
|
@@ -214,16 +235,24 @@ export function CorpusPanel({
|
|
|
214
235
|
await onChanged();
|
|
215
236
|
toast.success(`Deleted ${toDelete.length} item(s)`);
|
|
216
237
|
} catch (error) {
|
|
238
|
+
setDeletedIds((current) => {
|
|
239
|
+
const next = new Set(current);
|
|
240
|
+
for (const id of deletionIds) next.delete(id);
|
|
241
|
+
return next;
|
|
242
|
+
});
|
|
217
243
|
toast.error(formatErrorMessage(error) || 'Could not delete the selected documents.');
|
|
218
244
|
}
|
|
219
245
|
}
|
|
220
246
|
async function clearAll() {
|
|
247
|
+
setDeleteTask(null);
|
|
248
|
+
setClearingAll(true);
|
|
221
249
|
try {
|
|
222
250
|
await deleteRequest(workspaceUrl('/api/documents'));
|
|
223
251
|
setSelectedIds(new Set());
|
|
224
252
|
await onChanged();
|
|
225
253
|
toast.success('Corpus and vector index cleared');
|
|
226
254
|
} catch (error) {
|
|
255
|
+
setClearingAll(false);
|
|
227
256
|
toast.error(formatErrorMessage(error) || 'Could not clear the corpus.');
|
|
228
257
|
}
|
|
229
258
|
}
|
|
@@ -299,7 +328,7 @@ export function CorpusPanel({
|
|
|
299
328
|
return <CorpusSkeleton />;
|
|
300
329
|
}
|
|
301
330
|
|
|
302
|
-
if (
|
|
331
|
+
if (clearingAll || groupedDocuments.length === 0) {
|
|
303
332
|
return (
|
|
304
333
|
<div className="flex flex-col items-center justify-center rounded-lg px-6 py-14 text-center">
|
|
305
334
|
<div className="size-10 p-1.5 bg-white/60 border border-border rounded-md flex items-center justify-center">
|
|
@@ -1144,7 +1173,11 @@ function DocumentDialog({
|
|
|
1144
1173
|
|
|
1145
1174
|
function CorpusSkeleton() {
|
|
1146
1175
|
return (
|
|
1147
|
-
<div
|
|
1176
|
+
<div
|
|
1177
|
+
role="status"
|
|
1178
|
+
aria-label="Loading corpus"
|
|
1179
|
+
className="space-y-4 animate-in fade-in duration-150"
|
|
1180
|
+
>
|
|
1148
1181
|
{/* Filter bar skeleton */}
|
|
1149
1182
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center justify-between pb-3">
|
|
1150
1183
|
<div className="flex flex-wrap items-center gap-2">
|
|
@@ -83,6 +83,12 @@ interface ImageIndexingCapability {
|
|
|
83
83
|
message?: string;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
interface PreparingFile {
|
|
87
|
+
id: string;
|
|
88
|
+
name: string;
|
|
89
|
+
size: number;
|
|
90
|
+
}
|
|
91
|
+
|
|
86
92
|
const stagedFilesByScope = new Map<string, StagedFile[]>();
|
|
87
93
|
const UPLOAD_QUEUE_KEY = 'larkup.upload-queue.v1';
|
|
88
94
|
|
|
@@ -179,6 +185,7 @@ export function UploadPanel({
|
|
|
179
185
|
const [remoteUrl, setRemoteUrl] = useState('');
|
|
180
186
|
const [loadingRemoteFile, setLoadingRemoteFile] = useState(false);
|
|
181
187
|
const [saving, setSaving] = useState(false);
|
|
188
|
+
const [preparingFiles, setPreparingFiles] = useState<PreparingFile[]>([]);
|
|
182
189
|
const [progress, setProgress] = useState<{
|
|
183
190
|
message?: string;
|
|
184
191
|
current: number;
|
|
@@ -196,7 +203,17 @@ export function UploadPanel({
|
|
|
196
203
|
const joinedActiveSave = savingRef.current;
|
|
197
204
|
const next: StagedFile[] = [];
|
|
198
205
|
const stage = (file: StagedFile) => next.push({ ...file, sourceUrl });
|
|
199
|
-
|
|
206
|
+
const selectedFiles = Array.from(files).map((file) => ({
|
|
207
|
+
file,
|
|
208
|
+
pending: {
|
|
209
|
+
id: Math.random().toString(36).slice(2),
|
|
210
|
+
name: file.name,
|
|
211
|
+
size: file.size,
|
|
212
|
+
},
|
|
213
|
+
}));
|
|
214
|
+
setPreparingFiles((current) => [...current, ...selectedFiles.map(({ pending }) => pending)]);
|
|
215
|
+
|
|
216
|
+
for (const { file, pending } of selectedFiles) {
|
|
200
217
|
const ext = file.name.split('.').pop()?.toLowerCase();
|
|
201
218
|
|
|
202
219
|
// Prevent dragging media into the documents tab
|
|
@@ -205,10 +222,11 @@ export function UploadPanel({
|
|
|
205
222
|
toast.error(
|
|
206
223
|
`Unsupported file type: ${file.name}. Please upload media files in the Media tab.`,
|
|
207
224
|
);
|
|
225
|
+
setPreparingFiles((current) => current.filter((item) => item.id !== pending.id));
|
|
208
226
|
continue;
|
|
209
227
|
}
|
|
210
228
|
|
|
211
|
-
const id =
|
|
229
|
+
const id = pending.id;
|
|
212
230
|
try {
|
|
213
231
|
if (ext === 'csv') {
|
|
214
232
|
const content = await file.text();
|
|
@@ -320,12 +338,18 @@ export function UploadPanel({
|
|
|
320
338
|
});
|
|
321
339
|
|
|
322
340
|
const data = await res.json().catch(() => ({}));
|
|
323
|
-
|
|
341
|
+
const legacyImageOnlyPdf =
|
|
342
|
+
ext === 'pdf' &&
|
|
343
|
+
res.status === 422 &&
|
|
344
|
+
/(?:no readable text|does not contain selectable text)/i.test(String(data.error ?? ''));
|
|
345
|
+
if (!res.ok && !legacyImageOnlyPdf) {
|
|
324
346
|
throw new Error(data.error || `Could not extract text from ${file.name}`);
|
|
325
347
|
}
|
|
326
348
|
|
|
327
349
|
const { text } = data;
|
|
328
|
-
const hasReadableText =
|
|
350
|
+
const hasReadableText = legacyImageOnlyPdf
|
|
351
|
+
? false
|
|
352
|
+
: (data.hasReadableText ?? Boolean(text?.trim()));
|
|
329
353
|
const imageOnlyPdf = ext === 'pdf' && !hasReadableText;
|
|
330
354
|
if (!hasReadableText && !imageOnlyPdf) {
|
|
331
355
|
throw new Error(
|
|
@@ -363,6 +387,8 @@ export function UploadPanel({
|
|
|
363
387
|
}
|
|
364
388
|
} catch (err) {
|
|
365
389
|
toast.error(`Could not read ${file.name}. ${formatErrorMessage(err)}`);
|
|
390
|
+
} finally {
|
|
391
|
+
setPreparingFiles((current) => current.filter((item) => item.id !== pending.id));
|
|
366
392
|
}
|
|
367
393
|
}
|
|
368
394
|
setStaged((prev) => [...prev, ...next]);
|
|
@@ -687,7 +713,7 @@ export function UploadPanel({
|
|
|
687
713
|
}
|
|
688
714
|
|
|
689
715
|
useEffect(() => {
|
|
690
|
-
const actionBusy = saving || loadingRemoteFile;
|
|
716
|
+
const actionBusy = saving || loadingRemoteFile || preparingFiles.length > 0;
|
|
691
717
|
onActionChange?.({
|
|
692
718
|
label: 'Add file',
|
|
693
719
|
onClick: isRemoteEntry ? () => void loadRemoteFile() : () => void ingest(),
|
|
@@ -695,7 +721,16 @@ export function UploadPanel({
|
|
|
695
721
|
loading: actionBusy,
|
|
696
722
|
});
|
|
697
723
|
return () => onActionChange?.(null);
|
|
698
|
-
}, [
|
|
724
|
+
}, [
|
|
725
|
+
entryTab,
|
|
726
|
+
staged,
|
|
727
|
+
saving,
|
|
728
|
+
remoteUrl,
|
|
729
|
+
loadingRemoteFile,
|
|
730
|
+
preparingFiles.length,
|
|
731
|
+
groupId,
|
|
732
|
+
onActionChange,
|
|
733
|
+
]);
|
|
699
734
|
|
|
700
735
|
function updateEditingFile(patch: Partial<StagedFile>) {
|
|
701
736
|
setStaged((prev) => prev.map((f) => (f.id === editingFileId ? { ...f, ...patch } : f)));
|
|
@@ -775,6 +810,30 @@ export function UploadPanel({
|
|
|
775
810
|
}}
|
|
776
811
|
/>
|
|
777
812
|
|
|
813
|
+
{preparingFiles.length > 0 && (
|
|
814
|
+
<div
|
|
815
|
+
role="status"
|
|
816
|
+
aria-live="polite"
|
|
817
|
+
className="space-y-2 rounded-md border border-border bg-card px-3 py-3"
|
|
818
|
+
>
|
|
819
|
+
<div className="flex items-center gap-2 text-sm font-medium">
|
|
820
|
+
<Loader2 className="size-4 animate-spin text-primary" />
|
|
821
|
+
Preparing {preparingFiles.length} file{preparingFiles.length === 1 ? '' : 's'}…
|
|
822
|
+
</div>
|
|
823
|
+
<ul className="space-y-1">
|
|
824
|
+
{preparingFiles.map((file) => (
|
|
825
|
+
<li
|
|
826
|
+
key={file.id}
|
|
827
|
+
className="flex items-center justify-between gap-3 text-xs text-muted-foreground"
|
|
828
|
+
>
|
|
829
|
+
<span className="truncate font-mono">{file.name}</span>
|
|
830
|
+
<span className="shrink-0 tabular-nums">{(file.size / 1024).toFixed(1)} KB</span>
|
|
831
|
+
</li>
|
|
832
|
+
))}
|
|
833
|
+
</ul>
|
|
834
|
+
</div>
|
|
835
|
+
)}
|
|
836
|
+
|
|
778
837
|
{staged.length > 0 && (
|
|
779
838
|
<div className="space-y-2 pb-5">
|
|
780
839
|
{staged.some((file) => file.name.toLowerCase().endsWith('.pdf')) &&
|
|
@@ -987,10 +1046,13 @@ export function UploadPanel({
|
|
|
987
1046
|
<Button
|
|
988
1047
|
onClick={isRemoteEntry ? () => void loadRemoteFile() : () => void ingest()}
|
|
989
1048
|
disabled={
|
|
990
|
-
loadingRemoteFile ||
|
|
1049
|
+
loadingRemoteFile ||
|
|
1050
|
+
saving ||
|
|
1051
|
+
preparingFiles.length > 0 ||
|
|
1052
|
+
(isRemoteEntry ? !remoteUrl.trim() : staged.length === 0)
|
|
991
1053
|
}
|
|
992
1054
|
>
|
|
993
|
-
{loadingRemoteFile || saving ? (
|
|
1055
|
+
{loadingRemoteFile || saving || preparingFiles.length > 0 ? (
|
|
994
1056
|
<Loader2 className="size-4 animate-spin" />
|
|
995
1057
|
) : (
|
|
996
1058
|
<FileUp className="size-4" />
|