strapi-content-embeddings 0.1.7 → 0.1.9
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/README.md +69 -2
- package/dist/_chunks/{App-CA5bQnKQ.js → App-ByRBbkZn.js} +430 -7
- package/dist/_chunks/{App-C5NFY1UT.mjs → App-MjsTrWRS.mjs} +432 -9
- package/dist/_chunks/{index-CVCA8dDp.js → index-TWbcT-zJ.js} +110 -42
- package/dist/_chunks/{index-CIpGvEcJ.mjs → index-ifqYByO5.mjs} +111 -43
- package/dist/admin/index.js +1 -1
- package/dist/admin/index.mjs +1 -1
- package/dist/admin/src/components/custom/SyncModal.d.ts +7 -0
- package/dist/admin/src/utils/api.d.ts +48 -0
- package/dist/server/index.js +197 -64
- package/dist/server/index.mjs +195 -64
- package/dist/server/src/config/index.d.ts +3 -0
- package/dist/server/src/index.d.ts +1 -0
- package/dist/server/src/services/embeddings.d.ts +2 -2
- package/dist/server/src/services/sync.d.ts +1 -0
- package/dist/server/src/utils/preprocessing.d.ts +26 -0
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -11,8 +11,9 @@ A Strapi v5 plugin that creates vector embeddings from your content using OpenAI
|
|
|
11
11
|
- **Content Manager Integration**: Create embeddings directly from any content type's edit view
|
|
12
12
|
- **Standalone Embeddings**: Create embeddings independent of content types
|
|
13
13
|
- **Multiple Embedding Models**: Support for OpenAI's text-embedding-3-small, text-embedding-3-large, and text-embedding-ada-002
|
|
14
|
-
- **Database Sync**: Sync embeddings from Neon DB to Strapi
|
|
14
|
+
- **Database Sync**: Sync embeddings from Neon DB to Strapi via admin UI or API endpoints
|
|
15
15
|
- **Automatic Chunking**: Split large content into multiple embeddings with overlap for context preservation
|
|
16
|
+
- **Content Preprocessing**: Automatically strips HTML and Markdown formatting for cleaner embeddings
|
|
16
17
|
|
|
17
18
|
## Requirements
|
|
18
19
|
|
|
@@ -393,6 +394,73 @@ Each chunk embedding includes metadata:
|
|
|
393
394
|
}
|
|
394
395
|
```
|
|
395
396
|
|
|
397
|
+
## Content Preprocessing
|
|
398
|
+
|
|
399
|
+
The plugin automatically preprocesses content before creating embeddings to improve semantic search quality. This is enabled by default.
|
|
400
|
+
|
|
401
|
+
### What Gets Cleaned
|
|
402
|
+
|
|
403
|
+
- **HTML tags**: Stripped while preserving text content
|
|
404
|
+
- **Markdown syntax**: Headers (`#`), bold (`**`), italic (`*`), links, lists, code blocks
|
|
405
|
+
- **Whitespace**: Normalized (multiple spaces/newlines collapsed)
|
|
406
|
+
|
|
407
|
+
### Why Preprocess?
|
|
408
|
+
|
|
409
|
+
Raw markdown/HTML formatting adds noise to embeddings without adding semantic meaning:
|
|
410
|
+
|
|
411
|
+
```
|
|
412
|
+
Input: "## Features\n- **Fast** search\n- <b>Reliable</b>"
|
|
413
|
+
Output: "Features: Fast search. Reliable"
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
Both produce the same semantic meaning, but the cleaned version creates better embeddings for search.
|
|
417
|
+
|
|
418
|
+
### Configuration
|
|
419
|
+
|
|
420
|
+
Preprocessing is enabled by default. To disable:
|
|
421
|
+
|
|
422
|
+
```typescript
|
|
423
|
+
// config/plugins.ts
|
|
424
|
+
export default ({ env }) => ({
|
|
425
|
+
"strapi-content-embeddings": {
|
|
426
|
+
enabled: true,
|
|
427
|
+
config: {
|
|
428
|
+
openAIApiKey: env("OPENAI_API_KEY"),
|
|
429
|
+
neonConnectionString: env("NEON_CONNECTION_STRING"),
|
|
430
|
+
preprocessContent: false, // Disable preprocessing
|
|
431
|
+
},
|
|
432
|
+
},
|
|
433
|
+
});
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
> **Note**: The original content is always preserved in Strapi. Preprocessing only affects the text sent to OpenAI for embedding generation.
|
|
437
|
+
|
|
438
|
+
## Admin Sync UI
|
|
439
|
+
|
|
440
|
+
The plugin includes a built-in sync interface accessible from the admin panel. Click the **Sync** button in the Content Embeddings page header.
|
|
441
|
+
|
|
442
|
+
### Available Operations
|
|
443
|
+
|
|
444
|
+
| Operation | Description |
|
|
445
|
+
|-----------|-------------|
|
|
446
|
+
| **Check Status** | Compare Neon and Strapi databases, shows counts and differences |
|
|
447
|
+
| **Sync from Neon** | Import embeddings from Neon to Strapi (with preview option) |
|
|
448
|
+
| **Recreate All** | Delete all Neon embeddings and recreate from Strapi data |
|
|
449
|
+
|
|
450
|
+
### Sync Workflow
|
|
451
|
+
|
|
452
|
+
1. Click **Sync** button to open the sync modal
|
|
453
|
+
2. View current sync status (Neon vs Strapi counts)
|
|
454
|
+
3. Select **Sync from Neon** operation
|
|
455
|
+
4. Click **Preview Sync** to see what changes would be made
|
|
456
|
+
5. Review the results (Created/Updated/Removed counts)
|
|
457
|
+
6. Click **Apply Changes** to execute the sync
|
|
458
|
+
|
|
459
|
+
### Sync Options
|
|
460
|
+
|
|
461
|
+
- **Dry Run**: Preview changes without applying them (enabled by default)
|
|
462
|
+
- **Remove Orphans**: Delete Strapi entries that don't exist in Neon
|
|
463
|
+
|
|
396
464
|
## How It Works
|
|
397
465
|
|
|
398
466
|
1. **Embedding Creation**: When you create an embedding, the content is sent to OpenAI's embedding API to generate a vector representation (1536 or 3072 dimensions depending on the model).
|
|
@@ -460,4 +528,3 @@ Configure these in **Settings > Roles** for each admin role.
|
|
|
460
528
|
## License
|
|
461
529
|
|
|
462
530
|
MIT
|
|
463
|
-
# strapi-content-embeddings
|
|
@@ -7,7 +7,7 @@ const react = require("react");
|
|
|
7
7
|
const designSystem = require("@strapi/design-system");
|
|
8
8
|
const icons = require("@strapi/icons");
|
|
9
9
|
const qs = require("qs");
|
|
10
|
-
const index = require("./index-
|
|
10
|
+
const index = require("./index-TWbcT-zJ.js");
|
|
11
11
|
const styled = require("styled-components");
|
|
12
12
|
const ReactMarkdown = require("react-markdown");
|
|
13
13
|
const reactIntl = require("react-intl");
|
|
@@ -468,6 +468,383 @@ function ChatModal() {
|
|
|
468
468
|
] }) })
|
|
469
469
|
] });
|
|
470
470
|
}
|
|
471
|
+
const SYNC_BASE = `/${index.PLUGIN_ID}`;
|
|
472
|
+
const syncApi = {
|
|
473
|
+
getStatus: async (fetchClient) => {
|
|
474
|
+
const response = await fetchClient.get(`${SYNC_BASE}/sync/status`);
|
|
475
|
+
return response.data;
|
|
476
|
+
},
|
|
477
|
+
syncFromNeon: async (fetchClient, options) => {
|
|
478
|
+
const queryString = options ? `?${qs__default.default.stringify(options)}` : "";
|
|
479
|
+
const response = await fetchClient.post(`${SYNC_BASE}/sync${queryString}`);
|
|
480
|
+
return response.data;
|
|
481
|
+
},
|
|
482
|
+
recreateAll: async (fetchClient) => {
|
|
483
|
+
const response = await fetchClient.post(`${SYNC_BASE}/recreate`);
|
|
484
|
+
return response.data;
|
|
485
|
+
}
|
|
486
|
+
};
|
|
487
|
+
function StatCard({ label, value, subtitle, variant = "default" }) {
|
|
488
|
+
const bgColors = {
|
|
489
|
+
default: "neutral100",
|
|
490
|
+
success: "success100",
|
|
491
|
+
warning: "warning100",
|
|
492
|
+
danger: "danger100"
|
|
493
|
+
};
|
|
494
|
+
const textColors = {
|
|
495
|
+
default: "neutral800",
|
|
496
|
+
success: "success700",
|
|
497
|
+
warning: "warning700",
|
|
498
|
+
danger: "danger700"
|
|
499
|
+
};
|
|
500
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
501
|
+
designSystem.Box,
|
|
502
|
+
{
|
|
503
|
+
padding: 4,
|
|
504
|
+
background: bgColors[variant],
|
|
505
|
+
hasRadius: true,
|
|
506
|
+
borderColor: "neutral200",
|
|
507
|
+
style: { textAlign: "center", minWidth: "120px" },
|
|
508
|
+
children: [
|
|
509
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral600", style: { textTransform: "uppercase", fontSize: "11px" }, children: label }),
|
|
510
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { paddingTop: 1, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "alpha", textColor: textColors[variant], fontWeight: "bold", children: value }) }),
|
|
511
|
+
subtitle && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { paddingTop: 1, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral500", children: subtitle }) })
|
|
512
|
+
]
|
|
513
|
+
}
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
function SyncModal({ isOpen, onClose, onSyncComplete }) {
|
|
517
|
+
const fetchClient = admin.useFetchClient();
|
|
518
|
+
const [operation, setOperation] = react.useState("status");
|
|
519
|
+
const [removeOrphans, setRemoveOrphans] = react.useState(false);
|
|
520
|
+
const [dryRun, setDryRun] = react.useState(true);
|
|
521
|
+
const [isLoading, setIsLoading] = react.useState(false);
|
|
522
|
+
const [status, setStatus] = react.useState(null);
|
|
523
|
+
const [syncResult, setSyncResult] = react.useState(null);
|
|
524
|
+
const [recreateResult, setRecreateResult] = react.useState(null);
|
|
525
|
+
const [error, setError] = react.useState(null);
|
|
526
|
+
react.useEffect(() => {
|
|
527
|
+
if (isOpen) {
|
|
528
|
+
fetchStatus();
|
|
529
|
+
}
|
|
530
|
+
}, [isOpen]);
|
|
531
|
+
react.useEffect(() => {
|
|
532
|
+
if (!isOpen) {
|
|
533
|
+
setSyncResult(null);
|
|
534
|
+
setRecreateResult(null);
|
|
535
|
+
setError(null);
|
|
536
|
+
setOperation("status");
|
|
537
|
+
setDryRun(true);
|
|
538
|
+
setRemoveOrphans(false);
|
|
539
|
+
}
|
|
540
|
+
}, [isOpen]);
|
|
541
|
+
const fetchStatus = async () => {
|
|
542
|
+
setIsLoading(true);
|
|
543
|
+
setError(null);
|
|
544
|
+
try {
|
|
545
|
+
const result = await syncApi.getStatus(fetchClient);
|
|
546
|
+
setStatus(result);
|
|
547
|
+
} catch (err) {
|
|
548
|
+
setError(err.message || "Failed to fetch sync status");
|
|
549
|
+
} finally {
|
|
550
|
+
setIsLoading(false);
|
|
551
|
+
}
|
|
552
|
+
};
|
|
553
|
+
const handleExecute = async () => {
|
|
554
|
+
setIsLoading(true);
|
|
555
|
+
setError(null);
|
|
556
|
+
setSyncResult(null);
|
|
557
|
+
setRecreateResult(null);
|
|
558
|
+
try {
|
|
559
|
+
if (operation === "status") {
|
|
560
|
+
await fetchStatus();
|
|
561
|
+
} else if (operation === "sync") {
|
|
562
|
+
const result = await syncApi.syncFromNeon(fetchClient, { removeOrphans, dryRun });
|
|
563
|
+
setSyncResult(result);
|
|
564
|
+
if (!dryRun && onSyncComplete) {
|
|
565
|
+
onSyncComplete();
|
|
566
|
+
}
|
|
567
|
+
} else if (operation === "recreate") {
|
|
568
|
+
const result = await syncApi.recreateAll(fetchClient);
|
|
569
|
+
setRecreateResult(result);
|
|
570
|
+
if (onSyncComplete) {
|
|
571
|
+
onSyncComplete();
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
} catch (err) {
|
|
575
|
+
setError(err.message || "Operation failed");
|
|
576
|
+
} finally {
|
|
577
|
+
setIsLoading(false);
|
|
578
|
+
}
|
|
579
|
+
};
|
|
580
|
+
const renderStatus = () => {
|
|
581
|
+
if (!status) return null;
|
|
582
|
+
return /* @__PURE__ */ jsxRuntime.jsx(designSystem.Card, { padding: 5, background: "neutral0", shadow: "tableShadow", children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", gap: 4, children: [
|
|
583
|
+
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { justifyContent: "space-between", alignItems: "center", children: [
|
|
584
|
+
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 2, alignItems: "center", children: [
|
|
585
|
+
/* @__PURE__ */ jsxRuntime.jsx(icons.Database, {}),
|
|
586
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "delta", fontWeight: "bold", children: "Sync Status" })
|
|
587
|
+
] }),
|
|
588
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Badge, { active: status.inSync, size: "S", children: status.inSync ? "In Sync" : "Out of Sync" })
|
|
589
|
+
] }),
|
|
590
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Divider, {}),
|
|
591
|
+
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 4, justifyContent: "center", wrap: "wrap", children: [
|
|
592
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
593
|
+
StatCard,
|
|
594
|
+
{
|
|
595
|
+
label: "Neon DB",
|
|
596
|
+
value: status.neonCount,
|
|
597
|
+
subtitle: "embeddings"
|
|
598
|
+
}
|
|
599
|
+
),
|
|
600
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
601
|
+
StatCard,
|
|
602
|
+
{
|
|
603
|
+
label: "Strapi DB",
|
|
604
|
+
value: status.strapiCount,
|
|
605
|
+
subtitle: "embeddings"
|
|
606
|
+
}
|
|
607
|
+
)
|
|
608
|
+
] }),
|
|
609
|
+
!status.inSync && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
610
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Divider, {}),
|
|
611
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { padding: 3, background: "warning100", hasRadius: true, children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", gap: 2, children: [
|
|
612
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "sigma", textColor: "warning700", children: "Differences Found" }),
|
|
613
|
+
status.missingInStrapi > 0 && /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Typography, { variant: "pi", textColor: "warning700", children: [
|
|
614
|
+
"• ",
|
|
615
|
+
status.missingInStrapi,
|
|
616
|
+
" embeddings missing in Strapi (will be created)"
|
|
617
|
+
] }),
|
|
618
|
+
status.missingInNeon > 0 && /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Typography, { variant: "pi", textColor: "danger600", children: [
|
|
619
|
+
"• ",
|
|
620
|
+
status.missingInNeon,
|
|
621
|
+
" orphaned entries in Strapi (not in Neon)"
|
|
622
|
+
] }),
|
|
623
|
+
status.contentDifferences > 0 && /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Typography, { variant: "pi", textColor: "warning700", children: [
|
|
624
|
+
"• ",
|
|
625
|
+
status.contentDifferences,
|
|
626
|
+
" entries with content differences"
|
|
627
|
+
] })
|
|
628
|
+
] }) })
|
|
629
|
+
] })
|
|
630
|
+
] }) });
|
|
631
|
+
};
|
|
632
|
+
const renderSyncResult = () => {
|
|
633
|
+
if (!syncResult) return null;
|
|
634
|
+
return /* @__PURE__ */ jsxRuntime.jsx(designSystem.Card, { padding: 5, background: syncResult.success ? "success100" : "danger100", shadow: "tableShadow", children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", gap: 4, children: [
|
|
635
|
+
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { justifyContent: "space-between", alignItems: "center", children: [
|
|
636
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "delta", fontWeight: "bold", children: syncResult.dryRun ? "Preview Results" : "Sync Results" }),
|
|
637
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Badge, { active: syncResult.success, children: syncResult.success ? /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 1, alignItems: "center", children: [
|
|
638
|
+
/* @__PURE__ */ jsxRuntime.jsx(icons.Check, { width: 12, height: 12 }),
|
|
639
|
+
"Success"
|
|
640
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 1, alignItems: "center", children: [
|
|
641
|
+
/* @__PURE__ */ jsxRuntime.jsx(icons.Cross, { width: 12, height: 12 }),
|
|
642
|
+
"Failed"
|
|
643
|
+
] }) })
|
|
644
|
+
] }),
|
|
645
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Divider, {}),
|
|
646
|
+
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 3, justifyContent: "center", wrap: "wrap", children: [
|
|
647
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
648
|
+
StatCard,
|
|
649
|
+
{
|
|
650
|
+
label: "Created",
|
|
651
|
+
value: syncResult.actions.created,
|
|
652
|
+
variant: syncResult.actions.created > 0 ? "success" : "default"
|
|
653
|
+
}
|
|
654
|
+
),
|
|
655
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
656
|
+
StatCard,
|
|
657
|
+
{
|
|
658
|
+
label: "Updated",
|
|
659
|
+
value: syncResult.actions.updated,
|
|
660
|
+
variant: syncResult.actions.updated > 0 ? "warning" : "default"
|
|
661
|
+
}
|
|
662
|
+
),
|
|
663
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
664
|
+
StatCard,
|
|
665
|
+
{
|
|
666
|
+
label: "Removed",
|
|
667
|
+
value: syncResult.actions.orphansRemoved,
|
|
668
|
+
variant: syncResult.actions.orphansRemoved > 0 ? "danger" : "default"
|
|
669
|
+
}
|
|
670
|
+
)
|
|
671
|
+
] }),
|
|
672
|
+
syncResult.errors.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Box, { padding: 3, background: "danger100", hasRadius: true, children: [
|
|
673
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "sigma", textColor: "danger700", children: "Errors" }),
|
|
674
|
+
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Box, { paddingTop: 2, children: [
|
|
675
|
+
syncResult.errors.slice(0, 3).map((err, i) => /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Typography, { variant: "pi", textColor: "danger600", children: [
|
|
676
|
+
"• ",
|
|
677
|
+
err
|
|
678
|
+
] }, i)),
|
|
679
|
+
syncResult.errors.length > 3 && /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Typography, { variant: "pi", textColor: "danger600", fontWeight: "bold", children: [
|
|
680
|
+
"+ ",
|
|
681
|
+
syncResult.errors.length - 3,
|
|
682
|
+
" more errors"
|
|
683
|
+
] })
|
|
684
|
+
] })
|
|
685
|
+
] }),
|
|
686
|
+
syncResult.dryRun && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { paddingTop: 2, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Alert, { variant: "default", closeLabel: "Close", children: 'This was a preview. No changes were made. Click "Apply Changes" below to execute the sync.' }) })
|
|
687
|
+
] }) });
|
|
688
|
+
};
|
|
689
|
+
const renderRecreateResult = () => {
|
|
690
|
+
if (!recreateResult) return null;
|
|
691
|
+
return /* @__PURE__ */ jsxRuntime.jsx(designSystem.Card, { padding: 5, background: recreateResult.success ? "success100" : "danger100", shadow: "tableShadow", children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", gap: 4, children: [
|
|
692
|
+
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { justifyContent: "space-between", alignItems: "center", children: [
|
|
693
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "delta", fontWeight: "bold", children: "Recreate Results" }),
|
|
694
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Badge, { active: recreateResult.success, children: recreateResult.success ? "Success" : "Failed" })
|
|
695
|
+
] }),
|
|
696
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Divider, {}),
|
|
697
|
+
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 3, justifyContent: "center", wrap: "wrap", children: [
|
|
698
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
699
|
+
StatCard,
|
|
700
|
+
{
|
|
701
|
+
label: "Processed",
|
|
702
|
+
value: recreateResult.totalProcessed
|
|
703
|
+
}
|
|
704
|
+
),
|
|
705
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
706
|
+
StatCard,
|
|
707
|
+
{
|
|
708
|
+
label: "Created",
|
|
709
|
+
value: recreateResult.created,
|
|
710
|
+
variant: "success"
|
|
711
|
+
}
|
|
712
|
+
),
|
|
713
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
714
|
+
StatCard,
|
|
715
|
+
{
|
|
716
|
+
label: "Failed",
|
|
717
|
+
value: recreateResult.failed,
|
|
718
|
+
variant: recreateResult.failed > 0 ? "danger" : "default"
|
|
719
|
+
}
|
|
720
|
+
)
|
|
721
|
+
] }),
|
|
722
|
+
recreateResult.errors.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Box, { padding: 3, background: "danger100", hasRadius: true, children: [
|
|
723
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "sigma", textColor: "danger700", children: "Errors" }),
|
|
724
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { paddingTop: 2, children: recreateResult.errors.slice(0, 3).map((err, i) => /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Typography, { variant: "pi", textColor: "danger600", children: [
|
|
725
|
+
"• ",
|
|
726
|
+
err
|
|
727
|
+
] }, i)) })
|
|
728
|
+
] })
|
|
729
|
+
] }) });
|
|
730
|
+
};
|
|
731
|
+
const getButtonLabel = () => {
|
|
732
|
+
if (operation === "status") return "Refresh Status";
|
|
733
|
+
if (operation === "sync") return dryRun ? "Preview Sync" : "Run Sync";
|
|
734
|
+
if (operation === "recreate") return "Recreate All";
|
|
735
|
+
return "Execute";
|
|
736
|
+
};
|
|
737
|
+
const handleApplyChanges = async () => {
|
|
738
|
+
setIsLoading(true);
|
|
739
|
+
setError(null);
|
|
740
|
+
setSyncResult(null);
|
|
741
|
+
try {
|
|
742
|
+
const result = await syncApi.syncFromNeon(fetchClient, { removeOrphans, dryRun: false });
|
|
743
|
+
setSyncResult(result);
|
|
744
|
+
if (onSyncComplete) {
|
|
745
|
+
onSyncComplete();
|
|
746
|
+
}
|
|
747
|
+
} catch (err) {
|
|
748
|
+
setError(err.message || "Failed to apply changes");
|
|
749
|
+
} finally {
|
|
750
|
+
setIsLoading(false);
|
|
751
|
+
}
|
|
752
|
+
};
|
|
753
|
+
const showApplyButton = syncResult?.dryRun && syncResult?.success;
|
|
754
|
+
return /* @__PURE__ */ jsxRuntime.jsx(designSystem.Modal.Root, { open: isOpen, onOpenChange: (open) => !open && onClose(), children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Modal.Content, { children: [
|
|
755
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Modal.Header, { children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 2, alignItems: "center", children: [
|
|
756
|
+
/* @__PURE__ */ jsxRuntime.jsx(icons.ArrowClockwise, {}),
|
|
757
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Modal.Title, { children: "Database Sync" })
|
|
758
|
+
] }) }),
|
|
759
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Modal.Body, { children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", gap: 5, children: [
|
|
760
|
+
error && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Alert, { variant: "danger", closeLabel: "Close", onClose: () => setError(null), children: error }),
|
|
761
|
+
isLoading && !status ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { justifyContent: "center", padding: 6, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Loader, { children: "Loading status..." }) }) : renderStatus(),
|
|
762
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Card, { padding: 5, background: "neutral0", shadow: "tableShadow", children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", gap: 4, children: [
|
|
763
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "delta", fontWeight: "bold", children: "Select Operation" }),
|
|
764
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
765
|
+
designSystem.SingleSelect,
|
|
766
|
+
{
|
|
767
|
+
value: operation,
|
|
768
|
+
onChange: (value) => setOperation(value),
|
|
769
|
+
disabled: isLoading,
|
|
770
|
+
size: "M",
|
|
771
|
+
children: [
|
|
772
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.SingleSelectOption, { value: "status", children: "Check Status" }),
|
|
773
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.SingleSelectOption, { value: "sync", children: "Sync from Neon" }),
|
|
774
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.SingleSelectOption, { value: "recreate", children: "Recreate All (Danger)" })
|
|
775
|
+
]
|
|
776
|
+
}
|
|
777
|
+
),
|
|
778
|
+
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Typography, { variant: "pi", textColor: "neutral500", children: [
|
|
779
|
+
operation === "status" && "Compare Neon and Strapi databases without making changes.",
|
|
780
|
+
operation === "sync" && "Import embeddings from Neon DB to Strapi. Use this to restore missing entries.",
|
|
781
|
+
operation === "recreate" && "Delete all Neon embeddings and recreate them from Strapi data."
|
|
782
|
+
] })
|
|
783
|
+
] }) }),
|
|
784
|
+
operation === "sync" && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Card, { padding: 5, background: "neutral0", shadow: "tableShadow", children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", gap: 4, children: [
|
|
785
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "delta", fontWeight: "bold", children: "Sync Options" }),
|
|
786
|
+
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", gap: 3, children: [
|
|
787
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
788
|
+
designSystem.Checkbox,
|
|
789
|
+
{
|
|
790
|
+
checked: dryRun,
|
|
791
|
+
onCheckedChange: (checked) => setDryRun(checked),
|
|
792
|
+
children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", children: [
|
|
793
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "omega", fontWeight: "semiBold", children: "Dry Run" }),
|
|
794
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral500", children: "Preview changes without applying them" })
|
|
795
|
+
] })
|
|
796
|
+
}
|
|
797
|
+
),
|
|
798
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
799
|
+
designSystem.Checkbox,
|
|
800
|
+
{
|
|
801
|
+
checked: removeOrphans,
|
|
802
|
+
onCheckedChange: (checked) => setRemoveOrphans(checked),
|
|
803
|
+
children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", children: [
|
|
804
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "omega", fontWeight: "semiBold", children: "Remove Orphans" }),
|
|
805
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral500", children: "Delete Strapi entries that don't exist in Neon" })
|
|
806
|
+
] })
|
|
807
|
+
}
|
|
808
|
+
)
|
|
809
|
+
] })
|
|
810
|
+
] }) }),
|
|
811
|
+
operation === "recreate" && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Alert, { variant: "danger", closeLabel: "Close", children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", gap: 2, children: [
|
|
812
|
+
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 2, alignItems: "center", children: [
|
|
813
|
+
/* @__PURE__ */ jsxRuntime.jsx(icons.WarningCircle, {}),
|
|
814
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "omega", fontWeight: "bold", children: "Destructive Operation" })
|
|
815
|
+
] }),
|
|
816
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", children: "This will delete ALL embeddings in Neon and recreate them from Strapi data. This operation cannot be undone. Only use if embeddings are corrupted." })
|
|
817
|
+
] }) }),
|
|
818
|
+
syncResult && renderSyncResult(),
|
|
819
|
+
recreateResult && renderRecreateResult()
|
|
820
|
+
] }) }),
|
|
821
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Modal.Footer, { children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { justifyContent: "space-between", width: "100%", children: [
|
|
822
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Modal.Close, { children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { variant: "tertiary", children: "Cancel" }) }),
|
|
823
|
+
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 2, children: [
|
|
824
|
+
showApplyButton && /* @__PURE__ */ jsxRuntime.jsx(
|
|
825
|
+
designSystem.Button,
|
|
826
|
+
{
|
|
827
|
+
onClick: handleApplyChanges,
|
|
828
|
+
loading: isLoading,
|
|
829
|
+
startIcon: /* @__PURE__ */ jsxRuntime.jsx(icons.Check, {}),
|
|
830
|
+
variant: "success",
|
|
831
|
+
children: "Apply Changes"
|
|
832
|
+
}
|
|
833
|
+
),
|
|
834
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
835
|
+
designSystem.Button,
|
|
836
|
+
{
|
|
837
|
+
onClick: handleExecute,
|
|
838
|
+
loading: isLoading,
|
|
839
|
+
startIcon: /* @__PURE__ */ jsxRuntime.jsx(icons.ArrowClockwise, {}),
|
|
840
|
+
variant: operation === "recreate" ? "danger" : "secondary",
|
|
841
|
+
children: getButtonLabel()
|
|
842
|
+
}
|
|
843
|
+
)
|
|
844
|
+
] })
|
|
845
|
+
] }) })
|
|
846
|
+
] }) });
|
|
847
|
+
}
|
|
471
848
|
const PAGE_SIZE = 10;
|
|
472
849
|
function debounce(func, wait) {
|
|
473
850
|
let timeout;
|
|
@@ -483,6 +860,7 @@ function HomePage() {
|
|
|
483
860
|
const [search, setSearch] = react.useState("");
|
|
484
861
|
const [isLoading, setIsLoading] = react.useState(true);
|
|
485
862
|
const [currentPage, setCurrentPage] = react.useState(1);
|
|
863
|
+
const [isSyncModalOpen, setIsSyncModalOpen] = react.useState(false);
|
|
486
864
|
const totalPages = embeddings ? Math.ceil(embeddings.totalCount / PAGE_SIZE) : 0;
|
|
487
865
|
const buildQuery = (searchTerm, page) => qs__default.default.stringify({
|
|
488
866
|
page,
|
|
@@ -519,18 +897,55 @@ function HomePage() {
|
|
|
519
897
|
const handleCreateNew = () => {
|
|
520
898
|
navigate(`/plugins/${index.PLUGIN_ID}/embeddings`);
|
|
521
899
|
};
|
|
900
|
+
const handleSyncComplete = () => {
|
|
901
|
+
fetchData(search, currentPage);
|
|
902
|
+
};
|
|
903
|
+
const headerActions = /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 2, children: [
|
|
904
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { variant: "secondary", startIcon: /* @__PURE__ */ jsxRuntime.jsx(icons.ArrowClockwise, {}), onClick: () => setIsSyncModalOpen(true), children: "Sync" }),
|
|
905
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { startIcon: /* @__PURE__ */ jsxRuntime.jsx(icons.Plus, {}), onClick: handleCreateNew, children: "Create new embedding" })
|
|
906
|
+
] });
|
|
522
907
|
if (isLoading && !embeddings) {
|
|
523
908
|
return /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Main, { children: [
|
|
524
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
909
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
910
|
+
admin.Layouts.Header,
|
|
911
|
+
{
|
|
912
|
+
title: "Content Embeddings",
|
|
913
|
+
subtitle: "Manage your content embeddings",
|
|
914
|
+
primaryAction: headerActions
|
|
915
|
+
}
|
|
916
|
+
),
|
|
525
917
|
/* @__PURE__ */ jsxRuntime.jsx(admin.Layouts.Content, { children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { justifyContent: "center", padding: 8, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Loader, { children: "Loading..." }) }) }),
|
|
526
|
-
/* @__PURE__ */ jsxRuntime.jsx(ChatModal, {})
|
|
918
|
+
/* @__PURE__ */ jsxRuntime.jsx(ChatModal, {}),
|
|
919
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
920
|
+
SyncModal,
|
|
921
|
+
{
|
|
922
|
+
isOpen: isSyncModalOpen,
|
|
923
|
+
onClose: () => setIsSyncModalOpen(false),
|
|
924
|
+
onSyncComplete: handleSyncComplete
|
|
925
|
+
}
|
|
926
|
+
)
|
|
527
927
|
] });
|
|
528
928
|
}
|
|
529
929
|
if (embeddings?.totalCount === 0 && !search) {
|
|
530
930
|
return /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Main, { children: [
|
|
531
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
931
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
932
|
+
admin.Layouts.Header,
|
|
933
|
+
{
|
|
934
|
+
title: "Content Embeddings",
|
|
935
|
+
subtitle: "Manage your content embeddings",
|
|
936
|
+
primaryAction: headerActions
|
|
937
|
+
}
|
|
938
|
+
),
|
|
532
939
|
/* @__PURE__ */ jsxRuntime.jsx(admin.Layouts.Content, { children: /* @__PURE__ */ jsxRuntime.jsx(EmptyState, {}) }),
|
|
533
|
-
/* @__PURE__ */ jsxRuntime.jsx(ChatModal, {})
|
|
940
|
+
/* @__PURE__ */ jsxRuntime.jsx(ChatModal, {}),
|
|
941
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
942
|
+
SyncModal,
|
|
943
|
+
{
|
|
944
|
+
isOpen: isSyncModalOpen,
|
|
945
|
+
onClose: () => setIsSyncModalOpen(false),
|
|
946
|
+
onSyncComplete: handleSyncComplete
|
|
947
|
+
}
|
|
948
|
+
)
|
|
534
949
|
] });
|
|
535
950
|
}
|
|
536
951
|
const renderEmbeddingsContent = () => {
|
|
@@ -612,7 +1027,7 @@ function HomePage() {
|
|
|
612
1027
|
{
|
|
613
1028
|
title: "Content Embeddings",
|
|
614
1029
|
subtitle: `${embeddings?.totalCount || 0} embeddings total`,
|
|
615
|
-
primaryAction:
|
|
1030
|
+
primaryAction: headerActions
|
|
616
1031
|
}
|
|
617
1032
|
),
|
|
618
1033
|
/* @__PURE__ */ jsxRuntime.jsxs(admin.Layouts.Content, { children: [
|
|
@@ -629,7 +1044,15 @@ function HomePage() {
|
|
|
629
1044
|
renderEmbeddingsContent(),
|
|
630
1045
|
renderPagination()
|
|
631
1046
|
] }),
|
|
632
|
-
/* @__PURE__ */ jsxRuntime.jsx(ChatModal, {})
|
|
1047
|
+
/* @__PURE__ */ jsxRuntime.jsx(ChatModal, {}),
|
|
1048
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1049
|
+
SyncModal,
|
|
1050
|
+
{
|
|
1051
|
+
isOpen: isSyncModalOpen,
|
|
1052
|
+
onClose: () => setIsSyncModalOpen(false),
|
|
1053
|
+
onSyncComplete: handleSyncComplete
|
|
1054
|
+
}
|
|
1055
|
+
)
|
|
633
1056
|
] });
|
|
634
1057
|
}
|
|
635
1058
|
function CreateEmbeddingsForm({
|