@larkup/workspace-app 0.2.1 → 0.2.3
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/dist-types/components/data/corpus-panel.d.ts +2 -1
- package/dist-types/components/data/corpus-panel.d.ts.map +1 -1
- package/dist-types/components/data/data-workspace.d.ts.map +1 -1
- package/dist-types/components/data/project-data-page.d.ts.map +1 -1
- package/dist-types/components/data/upload-panel.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/components/data/corpus-panel.tsx +55 -5
- package/src/components/data/data-workspace.tsx +18 -4
- package/src/components/data/project-data-page.tsx +7 -1
- package/src/components/data/upload-panel.tsx +70 -8
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { SourceDocument, DataGroup } from '@larkup/core/types';
|
|
2
2
|
import { type VideoRuntimeScope } from '../../lib/media/video-runtime-scope';
|
|
3
|
-
export declare function CorpusPanel({ documents, groups, onChanged, isIndexing, activeVideoRuntimeScope, initialGroupId, isLoading, }: {
|
|
3
|
+
export declare function CorpusPanel({ documents, groups, onChanged, isIndexing, activeVideoRuntimeScope, initialGroupId, isLoading, error, }: {
|
|
4
4
|
documents: SourceDocument[];
|
|
5
5
|
groups?: DataGroup[];
|
|
6
6
|
onChanged: () => void | Promise<unknown>;
|
|
@@ -8,5 +8,6 @@ export declare function CorpusPanel({ documents, groups, onChanged, isIndexing,
|
|
|
8
8
|
activeVideoRuntimeScope: VideoRuntimeScope;
|
|
9
9
|
initialGroupId?: string;
|
|
10
10
|
isLoading?: boolean;
|
|
11
|
+
error?: string;
|
|
11
12
|
}): import("react").JSX.Element;
|
|
12
13
|
//# sourceMappingURL=corpus-panel.d.ts.map
|
|
@@ -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,
|
|
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,EACT,KAAK,GACN,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;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,+BAovBA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"data-workspace.d.ts","sourceRoot":"","sources":["../../../src/components/data/data-workspace.tsx"],"names":[],"mappings":"AAmEA,KAAK,QAAQ,GAAG,KAAK,GAAG,QAAQ,CAAC;AA+BjC,wBAAgB,aAAa,CAAC,EAAE,IAAI,EAAE,GAAE;IAAE,IAAI,CAAC,EAAE,QAAQ,CAAA;CAAO,+
|
|
1
|
+
{"version":3,"file":"data-workspace.d.ts","sourceRoot":"","sources":["../../../src/components/data/data-workspace.tsx"],"names":[],"mappings":"AAmEA,KAAK,QAAQ,GAAG,KAAK,GAAG,QAAQ,CAAC;AA+BjC,wBAAgB,aAAa,CAAC,EAAE,IAAI,EAAE,GAAE;IAAE,IAAI,CAAC,EAAE,QAAQ,CAAA;CAAO,+BA0hB/D"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"project-data-page.d.ts","sourceRoot":"","sources":["../../../src/components/data/project-data-page.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"project-data-page.d.ts","sourceRoot":"","sources":["../../../src/components/data/project-data-page.tsx"],"names":[],"mappings":"AAeA,kEAAkE;AAClE,wBAAgB,eAAe,gCA4C9B"}
|
|
@@ -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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@larkup/workspace-app",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Canonical Larkup chat, data and settings pages with host adapters",
|
|
6
6
|
"files": [
|
|
@@ -534,7 +534,7 @@
|
|
|
534
534
|
"dependencies": {
|
|
535
535
|
"@ai-sdk/react": "4.0.136",
|
|
536
536
|
"@base-ui/react": "^1.5.0",
|
|
537
|
-
"@larkup/core": "^0.8.
|
|
537
|
+
"@larkup/core": "^0.8.2",
|
|
538
538
|
"ai": "7.0.133",
|
|
539
539
|
"class-variance-authority": "^0.7.1",
|
|
540
540
|
"clsx": "^2.1.1",
|
|
@@ -556,9 +556,9 @@
|
|
|
556
556
|
"@larkup/vector-stores": "^0.1.27",
|
|
557
557
|
"@larkup/sandbox": "^0.1.8",
|
|
558
558
|
"next": "16.3.8",
|
|
559
|
-
"@larkup/
|
|
560
|
-
"@larkup/
|
|
561
|
-
"@larkup/
|
|
559
|
+
"@larkup/tool-doc-editor": "0.3.2",
|
|
560
|
+
"@larkup/workspace-ui": "0.2.0",
|
|
561
|
+
"@larkup/integrations": "0.2.1"
|
|
562
562
|
},
|
|
563
563
|
"peerDependencies": {
|
|
564
564
|
"react": ">=19",
|
|
@@ -88,6 +88,7 @@ export function CorpusPanel({
|
|
|
88
88
|
activeVideoRuntimeScope,
|
|
89
89
|
initialGroupId,
|
|
90
90
|
isLoading,
|
|
91
|
+
error,
|
|
91
92
|
}: {
|
|
92
93
|
documents: SourceDocument[];
|
|
93
94
|
groups?: DataGroup[];
|
|
@@ -96,12 +97,15 @@ export function CorpusPanel({
|
|
|
96
97
|
activeVideoRuntimeScope: VideoRuntimeScope;
|
|
97
98
|
initialGroupId?: string;
|
|
98
99
|
isLoading?: boolean;
|
|
100
|
+
error?: string;
|
|
99
101
|
}) {
|
|
100
102
|
const [active, setActive] = useState<SourceDocument | null>(null);
|
|
101
103
|
const [deleteTask, setDeleteTask] = useState<
|
|
102
104
|
{ type: 'single'; doc: SourceDocument } | { type: 'all' } | { type: 'selected' } | null
|
|
103
105
|
>(null);
|
|
104
106
|
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
|
|
107
|
+
const [deletedIds, setDeletedIds] = useState<Set<string>>(new Set());
|
|
108
|
+
const [clearingAll, setClearingAll] = useState(false);
|
|
105
109
|
const [movingGroupId, setMovingGroupId] = useState<string | null>(null);
|
|
106
110
|
const [inspectorDoc, setInspectorDoc] = useState<SourceDocument | null>(null);
|
|
107
111
|
|
|
@@ -143,9 +147,18 @@ export function CorpusPanel({
|
|
|
143
147
|
others.push(doc);
|
|
144
148
|
}
|
|
145
149
|
}
|
|
146
|
-
return [...Array.from(grouped.values()), ...others]
|
|
147
|
-
(
|
|
148
|
-
|
|
150
|
+
return [...Array.from(grouped.values()), ...others]
|
|
151
|
+
.filter((document) => !deletedIds.has(document.id))
|
|
152
|
+
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
|
153
|
+
}, [deletedIds, documents]);
|
|
154
|
+
|
|
155
|
+
useEffect(() => {
|
|
156
|
+
setDeletedIds((current) => {
|
|
157
|
+
const documentIds = new Set(documents.map((document) => document.id));
|
|
158
|
+
const pending = new Set(Array.from(current).filter((id) => documentIds.has(id)));
|
|
159
|
+
return pending.size === current.size ? current : pending;
|
|
160
|
+
});
|
|
161
|
+
if (documents.length === 0) setClearingAll(false);
|
|
149
162
|
}, [documents]);
|
|
150
163
|
|
|
151
164
|
const filteredDocuments = groupedDocuments.filter(
|
|
@@ -182,6 +195,8 @@ export function CorpusPanel({
|
|
|
182
195
|
}
|
|
183
196
|
|
|
184
197
|
async function del(id: string, doc?: SourceDocument) {
|
|
198
|
+
setDeleteTask(null);
|
|
199
|
+
setDeletedIds((current) => new Set(current).add(id));
|
|
185
200
|
try {
|
|
186
201
|
if (doc?.metadata?.isGroup && doc?.metadata?.mediaAssetId) {
|
|
187
202
|
await deleteRequest(workspaceUrl(`/api/media?id=${doc.metadata.mediaAssetId}`));
|
|
@@ -196,12 +211,20 @@ export function CorpusPanel({
|
|
|
196
211
|
await onChanged();
|
|
197
212
|
toast.success('Document deleted');
|
|
198
213
|
} catch (error) {
|
|
214
|
+
setDeletedIds((current) => {
|
|
215
|
+
const next = new Set(current);
|
|
216
|
+
next.delete(id);
|
|
217
|
+
return next;
|
|
218
|
+
});
|
|
199
219
|
toast.error(formatErrorMessage(error) || 'Could not delete this document.');
|
|
200
220
|
}
|
|
201
221
|
}
|
|
202
222
|
async function delSelected() {
|
|
203
223
|
if (selectedIds.size === 0) return;
|
|
204
224
|
const toDelete = groupedDocuments.filter((d) => selectedIds.has(d.id));
|
|
225
|
+
const deletionIds = new Set(toDelete.map((document) => document.id));
|
|
226
|
+
setDeleteTask(null);
|
|
227
|
+
setDeletedIds((current) => new Set([...current, ...deletionIds]));
|
|
205
228
|
try {
|
|
206
229
|
for (const doc of toDelete) {
|
|
207
230
|
if (doc.metadata?.isGroup && doc.metadata?.mediaAssetId) {
|
|
@@ -214,16 +237,24 @@ export function CorpusPanel({
|
|
|
214
237
|
await onChanged();
|
|
215
238
|
toast.success(`Deleted ${toDelete.length} item(s)`);
|
|
216
239
|
} catch (error) {
|
|
240
|
+
setDeletedIds((current) => {
|
|
241
|
+
const next = new Set(current);
|
|
242
|
+
for (const id of deletionIds) next.delete(id);
|
|
243
|
+
return next;
|
|
244
|
+
});
|
|
217
245
|
toast.error(formatErrorMessage(error) || 'Could not delete the selected documents.');
|
|
218
246
|
}
|
|
219
247
|
}
|
|
220
248
|
async function clearAll() {
|
|
249
|
+
setDeleteTask(null);
|
|
250
|
+
setClearingAll(true);
|
|
221
251
|
try {
|
|
222
252
|
await deleteRequest(workspaceUrl('/api/documents'));
|
|
223
253
|
setSelectedIds(new Set());
|
|
224
254
|
await onChanged();
|
|
225
255
|
toast.success('Corpus and vector index cleared');
|
|
226
256
|
} catch (error) {
|
|
257
|
+
setClearingAll(false);
|
|
227
258
|
toast.error(formatErrorMessage(error) || 'Could not clear the corpus.');
|
|
228
259
|
}
|
|
229
260
|
}
|
|
@@ -299,7 +330,22 @@ export function CorpusPanel({
|
|
|
299
330
|
return <CorpusSkeleton />;
|
|
300
331
|
}
|
|
301
332
|
|
|
302
|
-
if (
|
|
333
|
+
if (error) {
|
|
334
|
+
return (
|
|
335
|
+
<div className="flex flex-col items-center justify-center rounded-lg border border-border bg-card/50 px-6 py-14 text-center">
|
|
336
|
+
<div className="size-10 p-1.5 bg-destructive/10 border border-destructive/20 rounded-md flex items-center justify-center text-destructive">
|
|
337
|
+
<Info className="size-6" />
|
|
338
|
+
</div>
|
|
339
|
+
<p className="mt-3 text-lg font-medium text-foreground">Could not load corpus data</p>
|
|
340
|
+
<p className="mt-1 max-w-xl text-sm text-muted-foreground">{error}</p>
|
|
341
|
+
<Button variant="outline" size="sm" className="mt-4" onClick={() => void onChanged()}>
|
|
342
|
+
Retry
|
|
343
|
+
</Button>
|
|
344
|
+
</div>
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (clearingAll || groupedDocuments.length === 0) {
|
|
303
349
|
return (
|
|
304
350
|
<div className="flex flex-col items-center justify-center rounded-lg px-6 py-14 text-center">
|
|
305
351
|
<div className="size-10 p-1.5 bg-white/60 border border-border rounded-md flex items-center justify-center">
|
|
@@ -1144,7 +1190,11 @@ function DocumentDialog({
|
|
|
1144
1190
|
|
|
1145
1191
|
function CorpusSkeleton() {
|
|
1146
1192
|
return (
|
|
1147
|
-
<div
|
|
1193
|
+
<div
|
|
1194
|
+
role="status"
|
|
1195
|
+
aria-label="Loading corpus"
|
|
1196
|
+
className="space-y-4 animate-in fade-in duration-150"
|
|
1197
|
+
>
|
|
1148
1198
|
{/* Filter bar skeleton */}
|
|
1149
1199
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center justify-between pb-3">
|
|
1150
1200
|
<div className="flex flex-wrap items-center gap-2">
|
|
@@ -190,9 +190,13 @@ export function DataWorkspace({ view }: { view?: TopTabId } = {}) {
|
|
|
190
190
|
const completedJobs = jobs.filter((job) => job.status === 'completed');
|
|
191
191
|
const [jobsFilterTab, setJobsFilterTab] = useState<'running' | 'all'>('running');
|
|
192
192
|
|
|
193
|
-
const docsQuery = useSWR<DocsResponse>(
|
|
194
|
-
|
|
195
|
-
|
|
193
|
+
const docsQuery = useSWR<DocsResponse>(
|
|
194
|
+
serverId ? workspaceUrl('/api/documents') : null,
|
|
195
|
+
fetcher,
|
|
196
|
+
{
|
|
197
|
+
refreshInterval: hasActive ? 5000 : 0,
|
|
198
|
+
},
|
|
199
|
+
);
|
|
196
200
|
const { mutate: mutateDocuments } = docsQuery;
|
|
197
201
|
const documents = docsQuery.data?.documents ?? [];
|
|
198
202
|
const groups = docsQuery.data?.groups ?? [];
|
|
@@ -211,6 +215,9 @@ export function DataWorkspace({ view }: { view?: TopTabId } = {}) {
|
|
|
211
215
|
docsQuery.data !== undefined &&
|
|
212
216
|
!docsQuery.error &&
|
|
213
217
|
(targetGroupId === 'default' || selectedGroup !== undefined);
|
|
218
|
+
const isCorpusLoading = Boolean(
|
|
219
|
+
!serverId || docsQuery.isLoading || (docsQuery.data === undefined && !docsQuery.error),
|
|
220
|
+
);
|
|
214
221
|
|
|
215
222
|
useEffect(() => {
|
|
216
223
|
if (dataAddBlocked) setPrimaryAction(null);
|
|
@@ -613,7 +620,14 @@ export function DataWorkspace({ view }: { view?: TopTabId } = {}) {
|
|
|
613
620
|
isIndexing={indexRunning}
|
|
614
621
|
activeVideoRuntimeScope={activeVideoRuntimeScope}
|
|
615
622
|
initialGroupId={searchParams.has('groupId') ? queryGroupId : undefined}
|
|
616
|
-
isLoading={
|
|
623
|
+
isLoading={isCorpusLoading}
|
|
624
|
+
error={
|
|
625
|
+
docsQuery.error
|
|
626
|
+
? docsQuery.error instanceof Error
|
|
627
|
+
? docsQuery.error.message
|
|
628
|
+
: String(docsQuery.error)
|
|
629
|
+
: undefined
|
|
630
|
+
}
|
|
617
631
|
/>
|
|
618
632
|
</div>
|
|
619
633
|
)}
|
|
@@ -7,6 +7,7 @@ import { useRouter } from 'next/navigation';
|
|
|
7
7
|
import { Database, FolderOpen } from 'lucide-react';
|
|
8
8
|
import { WorkspaceLineTabs, WorkspacePageHeading } from '@larkup/workspace-ui';
|
|
9
9
|
import type { DataGroup } from '@larkup/core/types';
|
|
10
|
+
import { useProject } from '../projects/project-provider';
|
|
10
11
|
import { DataWorkspace } from './data-workspace';
|
|
11
12
|
import { GroupsPanel } from './groups-panel';
|
|
12
13
|
|
|
@@ -15,8 +16,13 @@ const fetcher = (url: string) => fetch(url).then((response) => response.json());
|
|
|
15
16
|
/** Simple, tab-first source management for the active Project. */
|
|
16
17
|
export function ProjectDataPage() {
|
|
17
18
|
const router = useRouter();
|
|
19
|
+
const { activeProject } = useProject();
|
|
20
|
+
const serverId = activeProject?.id;
|
|
18
21
|
const [activeView, setActiveView] = useState<'all' | 'groups'>('all');
|
|
19
|
-
const { data } = useSWR<{ groups: DataGroup[] }>(
|
|
22
|
+
const { data } = useSWR<{ groups: DataGroup[] }>(
|
|
23
|
+
serverId ? workspaceUrl('/api/groups') : null,
|
|
24
|
+
fetcher,
|
|
25
|
+
);
|
|
20
26
|
const groups = data?.groups ?? [];
|
|
21
27
|
|
|
22
28
|
return (
|
|
@@ -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" />
|