@alpaca-editor/core 1.0.4057 → 1.0.4061

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 (125) hide show
  1. package/dist/components/ui/badge.js.map +1 -1
  2. package/dist/components/ui/button.js.map +1 -1
  3. package/dist/components/ui/checkbox.js +1 -1
  4. package/dist/components/ui/checkbox.js.map +1 -1
  5. package/dist/components/ui/tooltip.js +2 -2
  6. package/dist/components/ui/tooltip.js.map +1 -1
  7. package/dist/config/config.js +7 -3
  8. package/dist/config/config.js.map +1 -1
  9. package/dist/config/types.d.ts +1 -0
  10. package/dist/editor/FieldListField.js +3 -2
  11. package/dist/editor/FieldListField.js.map +1 -1
  12. package/dist/editor/ai/AgentTerminal.js +0 -3
  13. package/dist/editor/ai/AgentTerminal.js.map +1 -1
  14. package/dist/editor/client/EditorClient.js +20 -6
  15. package/dist/editor/client/EditorClient.js.map +1 -1
  16. package/dist/editor/client/fieldModificationStore.d.ts +1 -0
  17. package/dist/editor/client/fieldModificationStore.js +19 -18
  18. package/dist/editor/client/fieldModificationStore.js.map +1 -1
  19. package/dist/editor/client/operations.js +1 -2
  20. package/dist/editor/client/operations.js.map +1 -1
  21. package/dist/editor/control-center/IndexOverview.js +278 -18
  22. package/dist/editor/control-center/IndexOverview.js.map +1 -1
  23. package/dist/editor/control-center/Status.js +1 -1
  24. package/dist/editor/control-center/Status.js.map +1 -1
  25. package/dist/editor/field-types/CheckboxEditor.js +1 -1
  26. package/dist/editor/field-types/CheckboxEditor.js.map +1 -1
  27. package/dist/editor/media-selector/AiImageSearch.js +2 -1
  28. package/dist/editor/media-selector/AiImageSearch.js.map +1 -1
  29. package/dist/editor/media-selector/TreeSelector.js +1 -1
  30. package/dist/editor/media-selector/TreeSelector.js.map +1 -1
  31. package/dist/editor/menubar/PageSelector.js +1 -1
  32. package/dist/editor/menubar/PageSelector.js.map +1 -1
  33. package/dist/editor/page-editor-chrome/useInlineAICompletion.js +1 -1
  34. package/dist/editor/page-editor-chrome/useInlineAICompletion.js.map +1 -1
  35. package/dist/editor/reviews/Comment.d.ts +5 -1
  36. package/dist/editor/reviews/Comment.js +64 -92
  37. package/dist/editor/reviews/Comment.js.map +1 -1
  38. package/dist/editor/reviews/CommentDisplayPopover.js +55 -49
  39. package/dist/editor/reviews/CommentDisplayPopover.js.map +1 -1
  40. package/dist/editor/reviews/CommentEditor.d.ts +20 -0
  41. package/dist/editor/reviews/CommentEditor.js +57 -0
  42. package/dist/editor/reviews/CommentEditor.js.map +1 -0
  43. package/dist/editor/reviews/CommentPopover.js +34 -43
  44. package/dist/editor/reviews/CommentPopover.js.map +1 -1
  45. package/dist/editor/reviews/CommentView.d.ts +19 -0
  46. package/dist/editor/reviews/CommentView.js +46 -0
  47. package/dist/editor/reviews/CommentView.js.map +1 -0
  48. package/dist/editor/reviews/Comments.js +28 -1
  49. package/dist/editor/reviews/Comments.js.map +1 -1
  50. package/dist/editor/reviews/SuggestionDisplayPopover.js +1 -1
  51. package/dist/editor/reviews/SuggestionDisplayPopover.js.map +1 -1
  52. package/dist/editor/services/aiService.d.ts +0 -8
  53. package/dist/editor/services/aiService.js +1 -24
  54. package/dist/editor/services/aiService.js.map +1 -1
  55. package/dist/editor/services/indexService.d.ts +12 -6
  56. package/dist/editor/services/indexService.js +33 -11
  57. package/dist/editor/services/indexService.js.map +1 -1
  58. package/dist/editor/services/reviewsService.d.ts +5 -1
  59. package/dist/editor/services/reviewsService.js +15 -0
  60. package/dist/editor/services/reviewsService.js.map +1 -1
  61. package/dist/editor/services/searchService.d.ts +17 -0
  62. package/dist/editor/services/searchService.js +25 -0
  63. package/dist/editor/services/searchService.js.map +1 -0
  64. package/dist/editor/sidebar/ViewSelector.js +7 -6
  65. package/dist/editor/sidebar/ViewSelector.js.map +1 -1
  66. package/dist/editor/ui/CenteredMessage.js +1 -1
  67. package/dist/editor/ui/CenteredMessage.js.map +1 -1
  68. package/dist/editor/ui/ItemSearch.js +1 -1
  69. package/dist/editor/ui/ItemSearch.js.map +1 -1
  70. package/dist/editor/ui/SimpleToolbar.js +1 -1
  71. package/dist/editor/ui/SimpleToolbar.js.map +1 -1
  72. package/dist/editor/ui/Spinner.d.ts +2 -1
  73. package/dist/editor/ui/Spinner.js +3 -2
  74. package/dist/editor/ui/Spinner.js.map +1 -1
  75. package/dist/editor/utils/keyboardNavigation.js +3 -1
  76. package/dist/editor/utils/keyboardNavigation.js.map +1 -1
  77. package/dist/page-wizard/steps/ImagesStep.js +2 -1
  78. package/dist/page-wizard/steps/ImagesStep.js.map +1 -1
  79. package/dist/revision.d.ts +2 -2
  80. package/dist/revision.js +2 -2
  81. package/dist/styles.css +76 -12
  82. package/dist/types.d.ts +61 -19
  83. package/package.json +1 -1
  84. package/src/components/ui/badge.tsx +1 -1
  85. package/src/components/ui/button.tsx +1 -1
  86. package/src/components/ui/checkbox.tsx +1 -1
  87. package/src/components/ui/tooltip.tsx +5 -3
  88. package/src/config/config.tsx +9 -3
  89. package/src/config/types.ts +1 -0
  90. package/src/editor/FieldListField.tsx +51 -32
  91. package/src/editor/ai/AgentTerminal.tsx +0 -6
  92. package/src/editor/client/EditorClient.tsx +47 -15
  93. package/src/editor/client/fieldModificationStore.ts +19 -19
  94. package/src/editor/client/operations.ts +1 -3
  95. package/src/editor/control-center/IndexOverview.tsx +590 -36
  96. package/src/editor/control-center/Status.tsx +1 -1
  97. package/src/editor/field-types/CheckboxEditor.tsx +1 -0
  98. package/src/editor/media-selector/AiImageSearch.tsx +2 -1
  99. package/src/editor/media-selector/TreeSelector.tsx +1 -1
  100. package/src/editor/menubar/PageSelector.tsx +1 -1
  101. package/src/editor/page-editor-chrome/useInlineAICompletion.tsx +1 -1
  102. package/src/editor/reviews/Comment.tsx +105 -244
  103. package/src/editor/reviews/CommentDisplayPopover.tsx +90 -219
  104. package/src/editor/reviews/CommentEditor.tsx +153 -0
  105. package/src/editor/reviews/CommentPopover.tsx +42 -70
  106. package/src/editor/reviews/CommentView.tsx +229 -0
  107. package/src/editor/reviews/Comments.tsx +33 -1
  108. package/src/editor/reviews/SuggestionDisplayPopover.tsx +1 -1
  109. package/src/editor/services/aiService.ts +1 -39
  110. package/src/editor/services/indexService.ts +58 -12
  111. package/src/editor/services/reviewsService.ts +34 -1
  112. package/src/editor/services/searchService.ts +45 -0
  113. package/src/editor/sidebar/ViewSelector.tsx +40 -27
  114. package/src/editor/ui/CenteredMessage.tsx +1 -1
  115. package/src/editor/ui/ItemSearch.tsx +1 -1
  116. package/src/editor/ui/SimpleToolbar.tsx +1 -1
  117. package/src/editor/ui/Spinner.tsx +8 -1
  118. package/src/editor/utils/keyboardNavigation.ts +3 -1
  119. package/src/page-wizard/steps/ImagesStep.tsx +2 -1
  120. package/src/revision.ts +2 -2
  121. package/src/types.ts +74 -20
  122. package/dist/editor/control-center/IndexSettings.d.ts +0 -5
  123. package/dist/editor/control-center/IndexSettings.js +0 -104
  124. package/dist/editor/control-center/IndexSettings.js.map +0 -1
  125. package/src/editor/control-center/IndexSettings.tsx +0 -266
@@ -1,49 +1,603 @@
1
- import { useEffect } from "react";
2
- import { useState } from "react";
3
- import { getIndexes } from "../services/indexService";
1
+ import { useEffect, useState } from "react";
2
+ import {
3
+ RefreshCw,
4
+ Database,
5
+ Clock,
6
+ CheckCircle,
7
+ AlertCircle,
8
+ Trash2,
9
+ } from "lucide-react";
4
10
 
5
- import { IndexSettings } from "./IndexSettings";
6
11
  import { classNames } from "primereact/utils";
12
+ import {
13
+ IndexStatus,
14
+ StagingStatus,
15
+ ImportStatus,
16
+ CentroidsStatus,
17
+ } from "../../types";
18
+ import {
19
+ getIndexStatus,
20
+ cleanupBatches,
21
+ populateCentroids,
22
+ processCompletedBatches,
23
+ collectAndUpload,
24
+ submitStaged,
25
+ getStagingStatus,
26
+ getImportStatus,
27
+ getCentroidsStatus,
28
+ startDirectGeneration,
29
+ } from "../services/indexService";
7
30
 
8
31
  export function IndexOverview() {
9
- const [indexes, setIndexes] = useState<string[] | undefined>(undefined);
10
- const [selectedIndex, setSelectedIndex] = useState<string | undefined>(
11
- undefined
12
- );
32
+ const [indexStatus, setIndexStatus] = useState<IndexStatus | null>(null);
33
+ const [isLoading, setIsLoading] = useState(false);
34
+ const [isCleaning, setIsCleaning] = useState(false);
35
+ const [isPopulating, setIsPopulating] = useState(false);
36
+ const [kCentroids, setKCentroids] = useState<number>(1024);
37
+ const [error, setError] = useState<string | null>(null);
38
+ const [isFinalizing, setIsFinalizing] = useState(false);
39
+ const [staging, setStaging] = useState<StagingStatus | null>(null);
40
+ const [importStatus, setImportStatus] = useState<ImportStatus | null>(null);
41
+ const [centroidsStatus, setCentroidsStatus] =
42
+ useState<CentroidsStatus | null>(null);
43
+ const [isCollecting, setIsCollecting] = useState(false);
44
+ const [isSubmitting, setIsSubmitting] = useState(false);
45
+ const [isStartingDirect, setIsStartingDirect] = useState(false);
46
+
47
+ const fetchStatus = async () => {
48
+ try {
49
+ setIsLoading(true);
50
+ setError(null);
51
+ const status = await getIndexStatus();
52
+ setIndexStatus(status || null);
53
+ const st = (await getStagingStatus()) as StagingStatus | null;
54
+ setStaging(st || null);
55
+ const importSt = (await getImportStatus()) as ImportStatus | null;
56
+ setImportStatus(importSt || null);
57
+ const centroidsSt =
58
+ (await getCentroidsStatus()) as CentroidsStatus | null;
59
+ setCentroidsStatus(centroidsSt || null);
60
+ } catch (err) {
61
+ setError(
62
+ err instanceof Error ? err.message : "Failed to fetch index status",
63
+ );
64
+ } finally {
65
+ setIsLoading(false);
66
+ }
67
+ };
68
+
69
+ const handleCollectAndUpload = async () => {
70
+ try {
71
+ setIsCollecting(true);
72
+ setError(null);
73
+ await collectAndUpload();
74
+ await fetchStatus();
75
+ } catch (err) {
76
+ setError(
77
+ err instanceof Error ? err.message : "Failed to collect and upload",
78
+ );
79
+ } finally {
80
+ setIsCollecting(false);
81
+ }
82
+ };
83
+
84
+ const handleSubmitStaged = async () => {
85
+ try {
86
+ setIsSubmitting(true);
87
+ setError(null);
88
+ await submitStaged();
89
+ await fetchStatus();
90
+ } catch (err) {
91
+ setError(
92
+ err instanceof Error ? err.message : "Failed to submit staged files",
93
+ );
94
+ } finally {
95
+ setIsSubmitting(false);
96
+ }
97
+ };
98
+
99
+ const handleStartDirect = async () => {
100
+ try {
101
+ setIsStartingDirect(true);
102
+ setError(null);
103
+ await startDirectGeneration();
104
+ await fetchStatus();
105
+ } catch (err) {
106
+ setError(
107
+ err instanceof Error
108
+ ? err.message
109
+ : "Failed to start direct generation",
110
+ );
111
+ } finally {
112
+ setIsStartingDirect(false);
113
+ }
114
+ };
115
+
116
+ const handleCleanup = async () => {
117
+ try {
118
+ setIsCleaning(true);
119
+ setError(null);
120
+ await cleanupBatches();
121
+ await fetchStatus();
122
+ } catch (err) {
123
+ setError(
124
+ err instanceof Error ? err.message : "Failed to cleanup batches",
125
+ );
126
+ } finally {
127
+ setIsCleaning(false);
128
+ }
129
+ };
130
+
131
+ const handlePopulateCentroids = async () => {
132
+ try {
133
+ setIsPopulating(true);
134
+ setError(null);
135
+ await populateCentroids(kCentroids || 1024);
136
+ await fetchStatus();
137
+ } catch (err) {
138
+ setError(
139
+ err instanceof Error ? err.message : "Failed to populate centroids",
140
+ );
141
+ } finally {
142
+ setIsPopulating(false);
143
+ }
144
+ };
145
+
146
+ const handleFinalize = async () => {
147
+ try {
148
+ setIsFinalizing(true);
149
+ setError(null);
150
+ await processCompletedBatches();
151
+ await fetchStatus();
152
+ } catch (err) {
153
+ setError(
154
+ err instanceof Error ? err.message : "Failed to finalize rebuild",
155
+ );
156
+ } finally {
157
+ setIsFinalizing(false);
158
+ }
159
+ };
160
+
13
161
  useEffect(() => {
14
- async function loadIndexes() {
15
- const indexes = await getIndexes();
16
- if (indexes.indexOf("master") === -1) {
17
- indexes.push("master");
18
- }
19
- if (indexes.indexOf("media") === -1) {
20
- indexes.push("media");
21
- }
22
- setIndexes(indexes);
23
- }
24
- loadIndexes();
162
+ fetchStatus();
163
+ // Poll for status updates every 60 seconds
164
+ const interval = setInterval(fetchStatus, 60000);
165
+ return () => clearInterval(interval);
25
166
  }, []);
167
+
168
+ const getBatchStatusIcon = (status?: string) => {
169
+ switch (status?.toLowerCase()) {
170
+ case "completed":
171
+ return (
172
+ <CheckCircle className="h-4 w-4 text-green-500" strokeWidth={1} />
173
+ );
174
+ case "failed":
175
+ return <AlertCircle className="h-4 w-4 text-red-500" strokeWidth={1} />;
176
+ case "processing":
177
+ return <Clock className="h-4 w-4 text-blue-500" strokeWidth={1} />;
178
+ default:
179
+ return (
180
+ <AlertCircle className="h-4 w-4 text-yellow-500" strokeWidth={1} />
181
+ );
182
+ }
183
+ };
184
+
185
+ const getBatchStatusLabelStyles = (status?: string) => {
186
+ switch (status?.toLowerCase()) {
187
+ case "completed":
188
+ return "bg-green-100 text-green-700";
189
+ case "failed":
190
+ return "bg-red-100 text-red-700";
191
+ case "processing":
192
+ return "bg-blue-100 text-blue-700";
193
+ default:
194
+ return "bg-yellow-100 text-yellow-700";
195
+ }
196
+ };
197
+
26
198
  return (
27
- <div className="flex items-stretch h-full">
28
- <div className="flex flex-col gap-2 border-r border-gray-200">
29
- <div className="flex items-center justify-center p-2 min-w-40 border-b border-gray-200">
30
- Index
199
+ <div className="flex h-full items-stretch">
200
+ <div className="flex min-w-96 flex-col gap-4 p-4">
201
+ <div className="flex items-center justify-between gap-4 border-b border-gray-200 pb-2">
202
+ <div className="flex items-center gap-2">
203
+ <Database className="h-5 w-5" strokeWidth={1} />
204
+ <span className="font-semibold">Search Index</span>
205
+ </div>
206
+ <div className="flex items-center gap-2">
207
+ {(() => {
208
+ const stagedCount = staging?.fileCount ?? 0;
209
+ const batches = indexStatus?.batches || [];
210
+ const completedCount = batches.filter(
211
+ (b) => (b.status || "").toLowerCase() === "completed",
212
+ ).length;
213
+ const inProgressCount = batches.filter((b) => {
214
+ const s = (b.status || "").toLowerCase();
215
+ return (
216
+ s === "submitted" || s === "processing" || s === "queued"
217
+ );
218
+ }).length;
219
+ if (
220
+ isCollecting ||
221
+ isSubmitting ||
222
+ inProgressCount > 0 ||
223
+ indexStatus?.rebuilding ||
224
+ importStatus?.isImporting ||
225
+ centroidsStatus?.isPopulating
226
+ ) {
227
+ return null;
228
+ } else if (stagedCount > 0) {
229
+ const batchCost =
230
+ staging?.estimatedBatchCost ?? staging?.estimatedCost ?? 0;
231
+ const directCost = staging?.estimatedDirectCost ?? 0;
232
+ return (
233
+ <div className="flex items-center gap-2">
234
+ <button
235
+ onClick={handleSubmitStaged}
236
+ disabled={isSubmitting || stagedCount === 0}
237
+ className={classNames(
238
+ "flex items-center gap-2 rounded px-3 py-1.5 text-sm font-medium transition-colors",
239
+ {
240
+ "bg-amber-600 text-white hover:bg-amber-700":
241
+ !isSubmitting && stagedCount > 0,
242
+ "cursor-not-allowed bg-gray-300 text-gray-500":
243
+ isSubmitting || stagedCount === 0,
244
+ },
245
+ )}
246
+ >
247
+ <CheckCircle className="h-4 w-4" strokeWidth={1} />
248
+ {isSubmitting
249
+ ? "Submitting..."
250
+ : `Batch (~$${batchCost.toFixed(2)})`}
251
+ </button>
252
+ <button
253
+ onClick={handleStartDirect}
254
+ disabled={isStartingDirect || stagedCount === 0}
255
+ className={classNames(
256
+ "flex items-center gap-2 rounded px-3 py-1.5 text-sm font-medium transition-colors",
257
+ {
258
+ "bg-rose-600 text-white hover:bg-rose-700":
259
+ !isStartingDirect && stagedCount > 0,
260
+ "cursor-not-allowed bg-gray-300 text-gray-500":
261
+ isStartingDirect || stagedCount === 0,
262
+ },
263
+ )}
264
+ >
265
+ <CheckCircle className="h-4 w-4" strokeWidth={1} />
266
+ {isStartingDirect
267
+ ? "Starting..."
268
+ : `Direct (~$${directCost.toFixed(2)})`}
269
+ </button>
270
+ </div>
271
+ );
272
+ } else if (completedCount > 0) {
273
+ return (
274
+ <button
275
+ onClick={handleFinalize}
276
+ disabled={isFinalizing}
277
+ className={classNames(
278
+ "flex items-center gap-2 rounded px-3 py-1.5 text-sm font-medium transition-colors",
279
+ {
280
+ "bg-green-600 text-white hover:bg-green-700":
281
+ !isFinalizing,
282
+ "cursor-not-allowed bg-gray-300 text-gray-500":
283
+ isFinalizing,
284
+ },
285
+ )}
286
+ >
287
+ <CheckCircle className="h-4 w-4" strokeWidth={1} />
288
+ {isFinalizing ? "Importing..." : "Import embeddings"}
289
+ </button>
290
+ );
291
+ }
292
+ return (
293
+ <button
294
+ onClick={handleCollectAndUpload}
295
+ disabled={isCollecting}
296
+ className={classNames(
297
+ "flex items-center gap-2 rounded px-3 py-1.5 text-sm font-medium transition-colors",
298
+ {
299
+ "bg-indigo-600 text-white hover:bg-indigo-700":
300
+ !isCollecting,
301
+ "cursor-not-allowed bg-gray-300 text-gray-500":
302
+ isCollecting,
303
+ },
304
+ )}
305
+ >
306
+ <RefreshCw className="h-4 w-4" strokeWidth={1} />
307
+ {isCollecting ? "Collecting..." : "Rebuild Index"}
308
+ </button>
309
+ );
310
+ })()}
311
+
312
+ <div className="flex items-center gap-2">
313
+ <input
314
+ type="number"
315
+ min={1}
316
+ value={kCentroids}
317
+ onChange={(e) =>
318
+ setKCentroids(parseInt(e.target.value) || 1024)
319
+ }
320
+ className="w-24 rounded border border-gray-300 px-2 py-1 text-sm"
321
+ />
322
+ <button
323
+ onClick={handlePopulateCentroids}
324
+ disabled={centroidsStatus?.isPopulating || isPopulating}
325
+ className={classNames(
326
+ "flex items-center gap-2 rounded px-3 py-1.5 text-sm font-medium transition-colors",
327
+ {
328
+ "bg-purple-500 text-white hover:bg-purple-600":
329
+ !centroidsStatus?.isPopulating && !isPopulating,
330
+ "cursor-not-allowed bg-gray-300 text-gray-500":
331
+ centroidsStatus?.isPopulating || isPopulating,
332
+ },
333
+ )}
334
+ >
335
+ <Database className="h-4 w-4" strokeWidth={1} />
336
+ {centroidsStatus?.isPopulating || isPopulating
337
+ ? centroidsStatus?.status || "Populating..."
338
+ : "Populate Centroids"}
339
+ </button>
340
+ <button
341
+ onClick={handleCleanup}
342
+ disabled={isCleaning}
343
+ className={classNames(
344
+ "flex items-center gap-2 rounded px-3 py-1.5 text-sm font-medium transition-colors",
345
+ {
346
+ "bg-gray-100 text-gray-700 hover:bg-gray-200": !isCleaning,
347
+ "cursor-not-allowed bg-gray-300 text-gray-500": isCleaning,
348
+ },
349
+ )}
350
+ >
351
+ <Trash2 className="h-4 w-4" strokeWidth={1} />
352
+ {isCleaning ? "Cleaning..." : "Cleanup"}
353
+ </button>
354
+ </div>
355
+ </div>
31
356
  </div>
32
- {indexes?.map((index) => (
33
- <div
34
- key={index}
35
- onClick={() => setSelectedIndex(index)}
36
- className={classNames(
37
- "cursor-pointer p-2 px-6 hover:bg-gray-100 text-sm",
38
- selectedIndex === index && "bg-gray-100"
357
+
358
+ {error && (
359
+ <div className="rounded border border-red-200 bg-red-50 p-3 text-sm text-red-700">
360
+ {error}
361
+ </div>
362
+ )}
363
+
364
+ {isLoading && !indexStatus ? (
365
+ <div className="flex items-center justify-center py-8">
366
+ <RefreshCw
367
+ className="h-6 w-6 animate-spin text-gray-400"
368
+ strokeWidth={1}
369
+ />
370
+ </div>
371
+ ) : indexStatus ? (
372
+ <div className="space-y-4">
373
+ <div className="grid grid-cols-2 gap-4">
374
+ <div className="rounded bg-gray-50 p-3">
375
+ <div className="text-sm text-gray-600">Indexed Items</div>
376
+ <div className="text-2xl font-semibold">
377
+ {indexStatus.numberOfIndexedItems?.toLocaleString() ?? "0"}
378
+ </div>
379
+ </div>
380
+ <div className="rounded bg-gray-50 p-3">
381
+ <div className="text-sm text-gray-600">Status</div>
382
+ {(() => {
383
+ const batches = indexStatus?.batches || [];
384
+ const inProgress = batches.some((b) => {
385
+ const s = (b.status || "").toLowerCase();
386
+ return (
387
+ s === "submitted" || s === "processing" || s === "queued"
388
+ );
389
+ });
390
+ const statusLabel = isCollecting
391
+ ? "Collecting items"
392
+ : isSubmitting || inProgress
393
+ ? "Generating Embeddings"
394
+ : importStatus?.isImporting
395
+ ? importStatus.status || "Importing embeddings"
396
+ : centroidsStatus?.isPopulating
397
+ ? centroidsStatus.status || "Populating centroids"
398
+ : indexStatus.rebuilding
399
+ ? "Rebuilding"
400
+ : "Ready";
401
+ const statusClass =
402
+ statusLabel === "Rebuilding"
403
+ ? "text-orange-600"
404
+ : statusLabel === "Generating Embeddings"
405
+ ? "text-blue-600"
406
+ : statusLabel === "Collecting items"
407
+ ? "text-indigo-600"
408
+ : importStatus?.isImporting
409
+ ? "text-purple-600"
410
+ : centroidsStatus?.isPopulating
411
+ ? "text-pink-600"
412
+ : "text-green-600";
413
+ return (
414
+ <div
415
+ className={classNames("text-sm font-medium", statusClass)}
416
+ >
417
+ {statusLabel}
418
+ </div>
419
+ );
420
+ })()}
421
+ </div>
422
+ </div>
423
+
424
+ {(() => {
425
+ const stagedCount = staging?.fileCount ?? 0;
426
+
427
+ // Only show staging section if there are staged files
428
+ if (stagedCount > 0) {
429
+ return (
430
+ <div className="rounded bg-gray-50 p-3">
431
+ <div className="flex items-center justify-between text-sm text-gray-600">
432
+ <span>Staged Files</span>
433
+ <span>{stagedCount}</span>
434
+ </div>
435
+ <div className="mt-1 flex items-center justify-between text-sm text-gray-600">
436
+ <span>Estimated Input Tokens</span>
437
+ <span>
438
+ {(staging?.estimatedInputTokens ?? 0).toLocaleString()}
439
+ </span>
440
+ </div>
441
+ <div className="mt-1 flex items-center justify-between text-sm text-gray-600">
442
+ <span>Estimated Batch Cost</span>
443
+ <span>
444
+ ~$
445
+ {(
446
+ staging?.estimatedBatchCost ??
447
+ staging?.estimatedCost ??
448
+ 0
449
+ ).toFixed(2)}
450
+ </span>
451
+ </div>
452
+ <div className="mt-1 flex items-center justify-between text-sm text-gray-600">
453
+ <span>Estimated Direct Cost</span>
454
+ <span>
455
+ ~${(staging?.estimatedDirectCost ?? 0).toFixed(2)}
456
+ </span>
457
+ </div>
458
+ <div className="mt-2 text-xs text-gray-700">
459
+ Next step:{" "}
460
+ <span className="font-medium">
461
+ Choose: Batch or Direct generation
462
+ </span>
463
+ </div>
464
+ </div>
465
+ );
466
+ }
467
+
468
+ // Show next step info even when no staged files (but not "Rebuild Index")
469
+ const batches = indexStatus?.batches || [];
470
+ const completedCount = batches.filter(
471
+ (b) => (b.status || "").toLowerCase() === "completed",
472
+ ).length;
473
+ const inProgress = batches.some((b) => {
474
+ const s = (b.status || "").toLowerCase();
475
+ return (
476
+ s === "submitted" || s === "processing" || s === "queued"
477
+ );
478
+ });
479
+ const nextStep =
480
+ isCollecting ||
481
+ isSubmitting ||
482
+ isStartingDirect ||
483
+ indexStatus?.rebuilding ||
484
+ importStatus?.isImporting ||
485
+ centroidsStatus?.isPopulating
486
+ ? ""
487
+ : inProgress
488
+ ? "Generating Embeddings"
489
+ : completedCount > 0
490
+ ? "Import embeddings"
491
+ : ""; // Don't show "Rebuild Index" as next step
492
+
493
+ if (nextStep) {
494
+ return (
495
+ <div className="rounded bg-gray-50 p-3">
496
+ <div className="text-xs text-gray-700">
497
+ Next step: <span className="font-medium">{nextStep}</span>
498
+ </div>
499
+ </div>
500
+ );
501
+ }
502
+
503
+ return null;
504
+ })()}
505
+
506
+ {indexStatus.batches && indexStatus.batches.length > 0 && (
507
+ <div>
508
+ <h3 className="mb-2 text-sm font-medium text-gray-700">
509
+ Recent Batches
510
+ </h3>
511
+ <div className="space-y-2 overflow-y-auto">
512
+ {indexStatus.batches.map((batch) => (
513
+ <div
514
+ key={batch.id}
515
+ className="rounded bg-gray-50 p-2 text-sm"
516
+ >
517
+ <div className="flex items-center justify-between">
518
+ <div className="flex items-center gap-2">
519
+ {getBatchStatusIcon(batch.status)}
520
+ <span
521
+ className={`rounded px-2 py-0.5 text-xs font-medium ${getBatchStatusLabelStyles(batch.status)}`}
522
+ >
523
+ {batch.status ?? "Unknown"}
524
+ </span>
525
+ <span className="font-mono text-xs">
526
+ {batch.id?.slice(-8) ?? "Unknown"}
527
+ </span>
528
+ </div>
529
+ <div className="text-right">
530
+ <div className="text-gray-600">
531
+ {batch.numberOfItems ?? 0} items
532
+ </div>
533
+ <div className="text-xs text-gray-500">
534
+ {batch.created
535
+ ? new Date(batch.created).toLocaleDateString()
536
+ : "Unknown"}
537
+ </div>
538
+ </div>
539
+ </div>
540
+ {((batch.firstErrors && batch.firstErrors.length > 0) ||
541
+ batch.errorMessage) && (
542
+ <div className="mt-1 text-xs text-red-600">
543
+ {(() => {
544
+ const messages =
545
+ batch.firstErrors && batch.firstErrors.length > 0
546
+ ? batch.firstErrors.slice(0, 3)
547
+ : batch.errorMessage
548
+ ? [batch.errorMessage]
549
+ : [];
550
+ const shownCount = messages.length;
551
+ const totalCount =
552
+ batch.errorCount && batch.errorCount > 0
553
+ ? batch.errorCount
554
+ : shownCount;
555
+ const suffix =
556
+ totalCount > shownCount
557
+ ? ` (+${totalCount - shownCount} more)`
558
+ : "";
559
+ return `${messages.join(" · ")}${suffix}`;
560
+ })()}
561
+ </div>
562
+ )}
563
+ </div>
564
+ ))}
565
+ </div>
566
+ </div>
39
567
  )}
40
- >
41
- {index}
568
+
569
+ {indexStatus.latestEmbeddedItems &&
570
+ indexStatus.latestEmbeddedItems.length > 0 && (
571
+ <div>
572
+ <h3 className="mb-2 text-sm font-medium text-gray-700">
573
+ Latest Embeddings Generated
574
+ </h3>
575
+ <div className="max-h-72 space-y-1 overflow-y-auto">
576
+ {indexStatus.latestEmbeddedItems.slice(0, 50).map((it) => (
577
+ <div
578
+ key={it.itemId + it.created}
579
+ className="flex items-center justify-between rounded bg-gray-50 p-2 text-sm"
580
+ >
581
+ <div className="min-w-0">
582
+ <div className="truncate font-medium">
583
+ {it.name || it.itemId.slice(-8)}
584
+ </div>
585
+ {it.path && (
586
+ <div className="truncate text-xs text-gray-500">
587
+ {it.path}
588
+ </div>
589
+ )}
590
+ </div>
591
+ <div className="ml-2 shrink-0 text-xs text-gray-500">
592
+ {new Date(it.created).toLocaleString()}
593
+ </div>
594
+ </div>
595
+ ))}
596
+ </div>
597
+ </div>
598
+ )}
42
599
  </div>
43
- ))}
44
- </div>
45
- <div className="flex-1 p-2">
46
- {selectedIndex && <IndexSettings index={{ name: selectedIndex }} />}
600
+ ) : null}
47
601
  </div>
48
602
  </div>
49
603
  );
@@ -91,7 +91,7 @@ export function Status() {
91
91
  name: "content",
92
92
  defaultSize: "auto",
93
93
  content: (
94
- <div className="absolute inset-0 overflow-auto">
94
+ <div className="absolute inset-0 overflow-auto rounded-xl bg-white">
95
95
  {selectedPanel ? (
96
96
  selectedPanel.content
97
97
  ) : (
@@ -34,6 +34,7 @@ export function CheckboxEditor({
34
34
  key={fieldItem.id + field.id + fieldItem.language + fieldItem.version}
35
35
  checked={field.value}
36
36
  disabled={readOnly}
37
+ className="focus-shadow"
37
38
  onCheckedChange={(checked) => {
38
39
  setIsUpdating(true);
39
40
  editContext.operations.editField({
@@ -2,7 +2,8 @@ import { useEffect, useState, useRef } from "react";
2
2
 
3
3
  import { useDebouncedCallback } from "use-debounce";
4
4
  import { createEditorAiContext } from "../ai/editorAiContext";
5
- import { executePrompt, executeSearch } from "../services/aiService";
5
+ import { executePrompt } from "../services/aiService";
6
+ import { executeSearch } from "../services/searchService";
6
7
  import { useEditContext } from "../client/editContext";
7
8
  import { ProgressSpinner } from "primereact/progressspinner";
8
9
  import { InputText } from "primereact/inputtext";
@@ -7,7 +7,7 @@ import { ItemTreeNodeData, getChildren } from "../services/contentService";
7
7
  import { useEditContext } from "../client/editContext";
8
8
  import { Button } from "../../components/ui/button";
9
9
  import { UploadButton } from "../../components/ui/upload-button";
10
- import { executeSearch } from "../services/aiService";
10
+ import { executeSearch } from "../services/searchService";
11
11
  import { ClockIcon, Upload } from "lucide-react";
12
12
 
13
13
  import DialogButtons from "../ui/DialogButtons";
@@ -11,7 +11,7 @@ import { ResultItem } from "../ui/ItemSearch";
11
11
  import { ScrollingContentTree } from "../ScrollingContentTree";
12
12
  import { ItemTreeNodeData } from "../services/contentService";
13
13
  import { SimpleIconButton } from "../ui/SimpleIconButton";
14
- import { executeSearch } from "../services/aiService";
14
+ import { executeSearch } from "../services/searchService";
15
15
  import { ItemList } from "../ui/ItemList";
16
16
  import { DotsVerticalIcon } from "@radix-ui/react-icons";
17
17
  import {