strapi-content-embeddings 0.1.6 → 0.1.8

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 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 with cron-compatible endpoints
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-CVCA8dDp.js");
10
+ const index = require("./index-DkNKkHgk.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(admin.Layouts.Header, { title: "Content Embeddings", subtitle: "Manage your content embeddings" }),
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(admin.Layouts.Header, { title: "Content Embeddings", subtitle: "Manage your content embeddings" }),
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: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { startIcon: /* @__PURE__ */ jsxRuntime.jsx(icons.Plus, {}), onClick: handleCreateNew, children: "Create new embedding" })
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({