strapi-content-embeddings 0.1.7 → 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.
@@ -2,10 +2,10 @@ import { jsxs, jsx, Fragment } from "react/jsx-runtime";
2
2
  import { useFetchClient, Layouts, useNotification, Page } from "@strapi/strapi/admin";
3
3
  import { Link, useNavigate, NavLink, useParams, Routes, Route } from "react-router-dom";
4
4
  import { useRef, useState, useEffect, useCallback, useMemo } from "react";
5
- import { EmptyStateLayout, Button, Tr, Box, Table, Thead, Th, Typography, VisuallyHidden, Tbody, Td, Flex, IconButton, Modal, TextInput, Link as Link$1, Accordion, Main, Loader, Pagination, PreviousLink, PageLink, NextLink, Field, Textarea, Badge, Grid, Dialog } from "@strapi/design-system";
6
- import { Plus, ArrowRight, Search, ArrowLeft, Cross, Check, Pencil, Trash } from "@strapi/icons";
5
+ import { EmptyStateLayout, Button, Tr, Box, Table, Thead, Th, Typography, VisuallyHidden, Tbody, Td, Flex, IconButton, Modal, TextInput, Link as Link$1, Accordion, Alert, Loader, Card, SingleSelect, SingleSelectOption, Checkbox, Badge, Divider, Main, Pagination, PreviousLink, PageLink, NextLink, Field, Textarea, Grid, Dialog } from "@strapi/design-system";
6
+ import { Plus, ArrowRight, ArrowClockwise, WarningCircle, Check, Database, Cross, Search, ArrowLeft, Pencil, Trash } from "@strapi/icons";
7
7
  import qs from "qs";
8
- import { P as PLUGIN_ID, R as RobotIcon, M as MarkdownEditor } from "./index-CIpGvEcJ.mjs";
8
+ import { P as PLUGIN_ID, R as RobotIcon, M as MarkdownEditor } from "./index-C58A29qR.mjs";
9
9
  import styled from "styled-components";
10
10
  import ReactMarkdown from "react-markdown";
11
11
  import { useIntl } from "react-intl";
@@ -462,6 +462,383 @@ function ChatModal() {
462
462
  ] }) })
463
463
  ] });
464
464
  }
465
+ const SYNC_BASE = `/${PLUGIN_ID}`;
466
+ const syncApi = {
467
+ getStatus: async (fetchClient) => {
468
+ const response = await fetchClient.get(`${SYNC_BASE}/sync/status`);
469
+ return response.data;
470
+ },
471
+ syncFromNeon: async (fetchClient, options) => {
472
+ const queryString = options ? `?${qs.stringify(options)}` : "";
473
+ const response = await fetchClient.post(`${SYNC_BASE}/sync${queryString}`);
474
+ return response.data;
475
+ },
476
+ recreateAll: async (fetchClient) => {
477
+ const response = await fetchClient.post(`${SYNC_BASE}/recreate`);
478
+ return response.data;
479
+ }
480
+ };
481
+ function StatCard({ label, value, subtitle, variant = "default" }) {
482
+ const bgColors = {
483
+ default: "neutral100",
484
+ success: "success100",
485
+ warning: "warning100",
486
+ danger: "danger100"
487
+ };
488
+ const textColors = {
489
+ default: "neutral800",
490
+ success: "success700",
491
+ warning: "warning700",
492
+ danger: "danger700"
493
+ };
494
+ return /* @__PURE__ */ jsxs(
495
+ Box,
496
+ {
497
+ padding: 4,
498
+ background: bgColors[variant],
499
+ hasRadius: true,
500
+ borderColor: "neutral200",
501
+ style: { textAlign: "center", minWidth: "120px" },
502
+ children: [
503
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", style: { textTransform: "uppercase", fontSize: "11px" }, children: label }),
504
+ /* @__PURE__ */ jsx(Box, { paddingTop: 1, children: /* @__PURE__ */ jsx(Typography, { variant: "alpha", textColor: textColors[variant], fontWeight: "bold", children: value }) }),
505
+ subtitle && /* @__PURE__ */ jsx(Box, { paddingTop: 1, children: /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral500", children: subtitle }) })
506
+ ]
507
+ }
508
+ );
509
+ }
510
+ function SyncModal({ isOpen, onClose, onSyncComplete }) {
511
+ const fetchClient = useFetchClient();
512
+ const [operation, setOperation] = useState("status");
513
+ const [removeOrphans, setRemoveOrphans] = useState(false);
514
+ const [dryRun, setDryRun] = useState(true);
515
+ const [isLoading, setIsLoading] = useState(false);
516
+ const [status, setStatus] = useState(null);
517
+ const [syncResult, setSyncResult] = useState(null);
518
+ const [recreateResult, setRecreateResult] = useState(null);
519
+ const [error, setError] = useState(null);
520
+ useEffect(() => {
521
+ if (isOpen) {
522
+ fetchStatus();
523
+ }
524
+ }, [isOpen]);
525
+ useEffect(() => {
526
+ if (!isOpen) {
527
+ setSyncResult(null);
528
+ setRecreateResult(null);
529
+ setError(null);
530
+ setOperation("status");
531
+ setDryRun(true);
532
+ setRemoveOrphans(false);
533
+ }
534
+ }, [isOpen]);
535
+ const fetchStatus = async () => {
536
+ setIsLoading(true);
537
+ setError(null);
538
+ try {
539
+ const result = await syncApi.getStatus(fetchClient);
540
+ setStatus(result);
541
+ } catch (err) {
542
+ setError(err.message || "Failed to fetch sync status");
543
+ } finally {
544
+ setIsLoading(false);
545
+ }
546
+ };
547
+ const handleExecute = async () => {
548
+ setIsLoading(true);
549
+ setError(null);
550
+ setSyncResult(null);
551
+ setRecreateResult(null);
552
+ try {
553
+ if (operation === "status") {
554
+ await fetchStatus();
555
+ } else if (operation === "sync") {
556
+ const result = await syncApi.syncFromNeon(fetchClient, { removeOrphans, dryRun });
557
+ setSyncResult(result);
558
+ if (!dryRun && onSyncComplete) {
559
+ onSyncComplete();
560
+ }
561
+ } else if (operation === "recreate") {
562
+ const result = await syncApi.recreateAll(fetchClient);
563
+ setRecreateResult(result);
564
+ if (onSyncComplete) {
565
+ onSyncComplete();
566
+ }
567
+ }
568
+ } catch (err) {
569
+ setError(err.message || "Operation failed");
570
+ } finally {
571
+ setIsLoading(false);
572
+ }
573
+ };
574
+ const renderStatus = () => {
575
+ if (!status) return null;
576
+ return /* @__PURE__ */ jsx(Card, { padding: 5, background: "neutral0", shadow: "tableShadow", children: /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 4, children: [
577
+ /* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "center", children: [
578
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", children: [
579
+ /* @__PURE__ */ jsx(Database, {}),
580
+ /* @__PURE__ */ jsx(Typography, { variant: "delta", fontWeight: "bold", children: "Sync Status" })
581
+ ] }),
582
+ /* @__PURE__ */ jsx(Badge, { active: status.inSync, size: "S", children: status.inSync ? "In Sync" : "Out of Sync" })
583
+ ] }),
584
+ /* @__PURE__ */ jsx(Divider, {}),
585
+ /* @__PURE__ */ jsxs(Flex, { gap: 4, justifyContent: "center", wrap: "wrap", children: [
586
+ /* @__PURE__ */ jsx(
587
+ StatCard,
588
+ {
589
+ label: "Neon DB",
590
+ value: status.neonCount,
591
+ subtitle: "embeddings"
592
+ }
593
+ ),
594
+ /* @__PURE__ */ jsx(
595
+ StatCard,
596
+ {
597
+ label: "Strapi DB",
598
+ value: status.strapiCount,
599
+ subtitle: "embeddings"
600
+ }
601
+ )
602
+ ] }),
603
+ !status.inSync && /* @__PURE__ */ jsxs(Fragment, { children: [
604
+ /* @__PURE__ */ jsx(Divider, {}),
605
+ /* @__PURE__ */ jsx(Box, { padding: 3, background: "warning100", hasRadius: true, children: /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 2, children: [
606
+ /* @__PURE__ */ jsx(Typography, { variant: "sigma", textColor: "warning700", children: "Differences Found" }),
607
+ status.missingInStrapi > 0 && /* @__PURE__ */ jsxs(Typography, { variant: "pi", textColor: "warning700", children: [
608
+ "• ",
609
+ status.missingInStrapi,
610
+ " embeddings missing in Strapi (will be created)"
611
+ ] }),
612
+ status.missingInNeon > 0 && /* @__PURE__ */ jsxs(Typography, { variant: "pi", textColor: "danger600", children: [
613
+ "• ",
614
+ status.missingInNeon,
615
+ " orphaned entries in Strapi (not in Neon)"
616
+ ] }),
617
+ status.contentDifferences > 0 && /* @__PURE__ */ jsxs(Typography, { variant: "pi", textColor: "warning700", children: [
618
+ "• ",
619
+ status.contentDifferences,
620
+ " entries with content differences"
621
+ ] })
622
+ ] }) })
623
+ ] })
624
+ ] }) });
625
+ };
626
+ const renderSyncResult = () => {
627
+ if (!syncResult) return null;
628
+ return /* @__PURE__ */ jsx(Card, { padding: 5, background: syncResult.success ? "success100" : "danger100", shadow: "tableShadow", children: /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 4, children: [
629
+ /* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "center", children: [
630
+ /* @__PURE__ */ jsx(Typography, { variant: "delta", fontWeight: "bold", children: syncResult.dryRun ? "Preview Results" : "Sync Results" }),
631
+ /* @__PURE__ */ jsx(Badge, { active: syncResult.success, children: syncResult.success ? /* @__PURE__ */ jsxs(Flex, { gap: 1, alignItems: "center", children: [
632
+ /* @__PURE__ */ jsx(Check, { width: 12, height: 12 }),
633
+ "Success"
634
+ ] }) : /* @__PURE__ */ jsxs(Flex, { gap: 1, alignItems: "center", children: [
635
+ /* @__PURE__ */ jsx(Cross, { width: 12, height: 12 }),
636
+ "Failed"
637
+ ] }) })
638
+ ] }),
639
+ /* @__PURE__ */ jsx(Divider, {}),
640
+ /* @__PURE__ */ jsxs(Flex, { gap: 3, justifyContent: "center", wrap: "wrap", children: [
641
+ /* @__PURE__ */ jsx(
642
+ StatCard,
643
+ {
644
+ label: "Created",
645
+ value: syncResult.actions.created,
646
+ variant: syncResult.actions.created > 0 ? "success" : "default"
647
+ }
648
+ ),
649
+ /* @__PURE__ */ jsx(
650
+ StatCard,
651
+ {
652
+ label: "Updated",
653
+ value: syncResult.actions.updated,
654
+ variant: syncResult.actions.updated > 0 ? "warning" : "default"
655
+ }
656
+ ),
657
+ /* @__PURE__ */ jsx(
658
+ StatCard,
659
+ {
660
+ label: "Removed",
661
+ value: syncResult.actions.orphansRemoved,
662
+ variant: syncResult.actions.orphansRemoved > 0 ? "danger" : "default"
663
+ }
664
+ )
665
+ ] }),
666
+ syncResult.errors.length > 0 && /* @__PURE__ */ jsxs(Box, { padding: 3, background: "danger100", hasRadius: true, children: [
667
+ /* @__PURE__ */ jsx(Typography, { variant: "sigma", textColor: "danger700", children: "Errors" }),
668
+ /* @__PURE__ */ jsxs(Box, { paddingTop: 2, children: [
669
+ syncResult.errors.slice(0, 3).map((err, i) => /* @__PURE__ */ jsxs(Typography, { variant: "pi", textColor: "danger600", children: [
670
+ "• ",
671
+ err
672
+ ] }, i)),
673
+ syncResult.errors.length > 3 && /* @__PURE__ */ jsxs(Typography, { variant: "pi", textColor: "danger600", fontWeight: "bold", children: [
674
+ "+ ",
675
+ syncResult.errors.length - 3,
676
+ " more errors"
677
+ ] })
678
+ ] })
679
+ ] }),
680
+ syncResult.dryRun && /* @__PURE__ */ jsx(Box, { paddingTop: 2, children: /* @__PURE__ */ jsx(Alert, { variant: "default", closeLabel: "Close", children: 'This was a preview. No changes were made. Click "Apply Changes" below to execute the sync.' }) })
681
+ ] }) });
682
+ };
683
+ const renderRecreateResult = () => {
684
+ if (!recreateResult) return null;
685
+ return /* @__PURE__ */ jsx(Card, { padding: 5, background: recreateResult.success ? "success100" : "danger100", shadow: "tableShadow", children: /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 4, children: [
686
+ /* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "center", children: [
687
+ /* @__PURE__ */ jsx(Typography, { variant: "delta", fontWeight: "bold", children: "Recreate Results" }),
688
+ /* @__PURE__ */ jsx(Badge, { active: recreateResult.success, children: recreateResult.success ? "Success" : "Failed" })
689
+ ] }),
690
+ /* @__PURE__ */ jsx(Divider, {}),
691
+ /* @__PURE__ */ jsxs(Flex, { gap: 3, justifyContent: "center", wrap: "wrap", children: [
692
+ /* @__PURE__ */ jsx(
693
+ StatCard,
694
+ {
695
+ label: "Processed",
696
+ value: recreateResult.totalProcessed
697
+ }
698
+ ),
699
+ /* @__PURE__ */ jsx(
700
+ StatCard,
701
+ {
702
+ label: "Created",
703
+ value: recreateResult.created,
704
+ variant: "success"
705
+ }
706
+ ),
707
+ /* @__PURE__ */ jsx(
708
+ StatCard,
709
+ {
710
+ label: "Failed",
711
+ value: recreateResult.failed,
712
+ variant: recreateResult.failed > 0 ? "danger" : "default"
713
+ }
714
+ )
715
+ ] }),
716
+ recreateResult.errors.length > 0 && /* @__PURE__ */ jsxs(Box, { padding: 3, background: "danger100", hasRadius: true, children: [
717
+ /* @__PURE__ */ jsx(Typography, { variant: "sigma", textColor: "danger700", children: "Errors" }),
718
+ /* @__PURE__ */ jsx(Box, { paddingTop: 2, children: recreateResult.errors.slice(0, 3).map((err, i) => /* @__PURE__ */ jsxs(Typography, { variant: "pi", textColor: "danger600", children: [
719
+ "• ",
720
+ err
721
+ ] }, i)) })
722
+ ] })
723
+ ] }) });
724
+ };
725
+ const getButtonLabel = () => {
726
+ if (operation === "status") return "Refresh Status";
727
+ if (operation === "sync") return dryRun ? "Preview Sync" : "Run Sync";
728
+ if (operation === "recreate") return "Recreate All";
729
+ return "Execute";
730
+ };
731
+ const handleApplyChanges = async () => {
732
+ setIsLoading(true);
733
+ setError(null);
734
+ setSyncResult(null);
735
+ try {
736
+ const result = await syncApi.syncFromNeon(fetchClient, { removeOrphans, dryRun: false });
737
+ setSyncResult(result);
738
+ if (onSyncComplete) {
739
+ onSyncComplete();
740
+ }
741
+ } catch (err) {
742
+ setError(err.message || "Failed to apply changes");
743
+ } finally {
744
+ setIsLoading(false);
745
+ }
746
+ };
747
+ const showApplyButton = syncResult?.dryRun && syncResult?.success;
748
+ return /* @__PURE__ */ jsx(Modal.Root, { open: isOpen, onOpenChange: (open) => !open && onClose(), children: /* @__PURE__ */ jsxs(Modal.Content, { children: [
749
+ /* @__PURE__ */ jsx(Modal.Header, { children: /* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", children: [
750
+ /* @__PURE__ */ jsx(ArrowClockwise, {}),
751
+ /* @__PURE__ */ jsx(Modal.Title, { children: "Database Sync" })
752
+ ] }) }),
753
+ /* @__PURE__ */ jsx(Modal.Body, { children: /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 5, children: [
754
+ error && /* @__PURE__ */ jsx(Alert, { variant: "danger", closeLabel: "Close", onClose: () => setError(null), children: error }),
755
+ isLoading && !status ? /* @__PURE__ */ jsx(Flex, { justifyContent: "center", padding: 6, children: /* @__PURE__ */ jsx(Loader, { children: "Loading status..." }) }) : renderStatus(),
756
+ /* @__PURE__ */ jsx(Card, { padding: 5, background: "neutral0", shadow: "tableShadow", children: /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 4, children: [
757
+ /* @__PURE__ */ jsx(Typography, { variant: "delta", fontWeight: "bold", children: "Select Operation" }),
758
+ /* @__PURE__ */ jsxs(
759
+ SingleSelect,
760
+ {
761
+ value: operation,
762
+ onChange: (value) => setOperation(value),
763
+ disabled: isLoading,
764
+ size: "M",
765
+ children: [
766
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "status", children: "Check Status" }),
767
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "sync", children: "Sync from Neon" }),
768
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "recreate", children: "Recreate All (Danger)" })
769
+ ]
770
+ }
771
+ ),
772
+ /* @__PURE__ */ jsxs(Typography, { variant: "pi", textColor: "neutral500", children: [
773
+ operation === "status" && "Compare Neon and Strapi databases without making changes.",
774
+ operation === "sync" && "Import embeddings from Neon DB to Strapi. Use this to restore missing entries.",
775
+ operation === "recreate" && "Delete all Neon embeddings and recreate them from Strapi data."
776
+ ] })
777
+ ] }) }),
778
+ operation === "sync" && /* @__PURE__ */ jsx(Card, { padding: 5, background: "neutral0", shadow: "tableShadow", children: /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 4, children: [
779
+ /* @__PURE__ */ jsx(Typography, { variant: "delta", fontWeight: "bold", children: "Sync Options" }),
780
+ /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 3, children: [
781
+ /* @__PURE__ */ jsx(
782
+ Checkbox,
783
+ {
784
+ checked: dryRun,
785
+ onCheckedChange: (checked) => setDryRun(checked),
786
+ children: /* @__PURE__ */ jsxs(Flex, { direction: "column", children: [
787
+ /* @__PURE__ */ jsx(Typography, { variant: "omega", fontWeight: "semiBold", children: "Dry Run" }),
788
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral500", children: "Preview changes without applying them" })
789
+ ] })
790
+ }
791
+ ),
792
+ /* @__PURE__ */ jsx(
793
+ Checkbox,
794
+ {
795
+ checked: removeOrphans,
796
+ onCheckedChange: (checked) => setRemoveOrphans(checked),
797
+ children: /* @__PURE__ */ jsxs(Flex, { direction: "column", children: [
798
+ /* @__PURE__ */ jsx(Typography, { variant: "omega", fontWeight: "semiBold", children: "Remove Orphans" }),
799
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral500", children: "Delete Strapi entries that don't exist in Neon" })
800
+ ] })
801
+ }
802
+ )
803
+ ] })
804
+ ] }) }),
805
+ operation === "recreate" && /* @__PURE__ */ jsx(Alert, { variant: "danger", closeLabel: "Close", children: /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 2, children: [
806
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", children: [
807
+ /* @__PURE__ */ jsx(WarningCircle, {}),
808
+ /* @__PURE__ */ jsx(Typography, { variant: "omega", fontWeight: "bold", children: "Destructive Operation" })
809
+ ] }),
810
+ /* @__PURE__ */ jsx(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." })
811
+ ] }) }),
812
+ syncResult && renderSyncResult(),
813
+ recreateResult && renderRecreateResult()
814
+ ] }) }),
815
+ /* @__PURE__ */ jsx(Modal.Footer, { children: /* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", width: "100%", children: [
816
+ /* @__PURE__ */ jsx(Modal.Close, { children: /* @__PURE__ */ jsx(Button, { variant: "tertiary", children: "Cancel" }) }),
817
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
818
+ showApplyButton && /* @__PURE__ */ jsx(
819
+ Button,
820
+ {
821
+ onClick: handleApplyChanges,
822
+ loading: isLoading,
823
+ startIcon: /* @__PURE__ */ jsx(Check, {}),
824
+ variant: "success",
825
+ children: "Apply Changes"
826
+ }
827
+ ),
828
+ /* @__PURE__ */ jsx(
829
+ Button,
830
+ {
831
+ onClick: handleExecute,
832
+ loading: isLoading,
833
+ startIcon: /* @__PURE__ */ jsx(ArrowClockwise, {}),
834
+ variant: operation === "recreate" ? "danger" : "secondary",
835
+ children: getButtonLabel()
836
+ }
837
+ )
838
+ ] })
839
+ ] }) })
840
+ ] }) });
841
+ }
465
842
  const PAGE_SIZE = 10;
466
843
  function debounce(func, wait) {
467
844
  let timeout;
@@ -477,6 +854,7 @@ function HomePage() {
477
854
  const [search, setSearch] = useState("");
478
855
  const [isLoading, setIsLoading] = useState(true);
479
856
  const [currentPage, setCurrentPage] = useState(1);
857
+ const [isSyncModalOpen, setIsSyncModalOpen] = useState(false);
480
858
  const totalPages = embeddings ? Math.ceil(embeddings.totalCount / PAGE_SIZE) : 0;
481
859
  const buildQuery = (searchTerm, page) => qs.stringify({
482
860
  page,
@@ -513,18 +891,55 @@ function HomePage() {
513
891
  const handleCreateNew = () => {
514
892
  navigate(`/plugins/${PLUGIN_ID}/embeddings`);
515
893
  };
894
+ const handleSyncComplete = () => {
895
+ fetchData(search, currentPage);
896
+ };
897
+ const headerActions = /* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
898
+ /* @__PURE__ */ jsx(Button, { variant: "secondary", startIcon: /* @__PURE__ */ jsx(ArrowClockwise, {}), onClick: () => setIsSyncModalOpen(true), children: "Sync" }),
899
+ /* @__PURE__ */ jsx(Button, { startIcon: /* @__PURE__ */ jsx(Plus, {}), onClick: handleCreateNew, children: "Create new embedding" })
900
+ ] });
516
901
  if (isLoading && !embeddings) {
517
902
  return /* @__PURE__ */ jsxs(Main, { children: [
518
- /* @__PURE__ */ jsx(Layouts.Header, { title: "Content Embeddings", subtitle: "Manage your content embeddings" }),
903
+ /* @__PURE__ */ jsx(
904
+ Layouts.Header,
905
+ {
906
+ title: "Content Embeddings",
907
+ subtitle: "Manage your content embeddings",
908
+ primaryAction: headerActions
909
+ }
910
+ ),
519
911
  /* @__PURE__ */ jsx(Layouts.Content, { children: /* @__PURE__ */ jsx(Flex, { justifyContent: "center", padding: 8, children: /* @__PURE__ */ jsx(Loader, { children: "Loading..." }) }) }),
520
- /* @__PURE__ */ jsx(ChatModal, {})
912
+ /* @__PURE__ */ jsx(ChatModal, {}),
913
+ /* @__PURE__ */ jsx(
914
+ SyncModal,
915
+ {
916
+ isOpen: isSyncModalOpen,
917
+ onClose: () => setIsSyncModalOpen(false),
918
+ onSyncComplete: handleSyncComplete
919
+ }
920
+ )
521
921
  ] });
522
922
  }
523
923
  if (embeddings?.totalCount === 0 && !search) {
524
924
  return /* @__PURE__ */ jsxs(Main, { children: [
525
- /* @__PURE__ */ jsx(Layouts.Header, { title: "Content Embeddings", subtitle: "Manage your content embeddings" }),
925
+ /* @__PURE__ */ jsx(
926
+ Layouts.Header,
927
+ {
928
+ title: "Content Embeddings",
929
+ subtitle: "Manage your content embeddings",
930
+ primaryAction: headerActions
931
+ }
932
+ ),
526
933
  /* @__PURE__ */ jsx(Layouts.Content, { children: /* @__PURE__ */ jsx(EmptyState, {}) }),
527
- /* @__PURE__ */ jsx(ChatModal, {})
934
+ /* @__PURE__ */ jsx(ChatModal, {}),
935
+ /* @__PURE__ */ jsx(
936
+ SyncModal,
937
+ {
938
+ isOpen: isSyncModalOpen,
939
+ onClose: () => setIsSyncModalOpen(false),
940
+ onSyncComplete: handleSyncComplete
941
+ }
942
+ )
528
943
  ] });
529
944
  }
530
945
  const renderEmbeddingsContent = () => {
@@ -606,7 +1021,7 @@ function HomePage() {
606
1021
  {
607
1022
  title: "Content Embeddings",
608
1023
  subtitle: `${embeddings?.totalCount || 0} embeddings total`,
609
- primaryAction: /* @__PURE__ */ jsx(Button, { startIcon: /* @__PURE__ */ jsx(Plus, {}), onClick: handleCreateNew, children: "Create new embedding" })
1024
+ primaryAction: headerActions
610
1025
  }
611
1026
  ),
612
1027
  /* @__PURE__ */ jsxs(Layouts.Content, { children: [
@@ -623,7 +1038,15 @@ function HomePage() {
623
1038
  renderEmbeddingsContent(),
624
1039
  renderPagination()
625
1040
  ] }),
626
- /* @__PURE__ */ jsx(ChatModal, {})
1041
+ /* @__PURE__ */ jsx(ChatModal, {}),
1042
+ /* @__PURE__ */ jsx(
1043
+ SyncModal,
1044
+ {
1045
+ isOpen: isSyncModalOpen,
1046
+ onClose: () => setIsSyncModalOpen(false),
1047
+ onSyncComplete: handleSyncComplete
1048
+ }
1049
+ )
627
1050
  ] });
628
1051
  }
629
1052
  function CreateEmbeddingsForm({
@@ -669,7 +669,7 @@ const index = {
669
669
  defaultMessage: PLUGIN_ID
670
670
  },
671
671
  Component: async () => {
672
- const { App } = await import("./App-C5NFY1UT.mjs");
672
+ const { App } = await import("./App-sRU0Nh3x.mjs");
673
673
  return App;
674
674
  }
675
675
  });
@@ -673,7 +673,7 @@ const index = {
673
673
  defaultMessage: PLUGIN_ID
674
674
  },
675
675
  Component: async () => {
676
- const { App } = await Promise.resolve().then(() => require("./App-CA5bQnKQ.js"));
676
+ const { App } = await Promise.resolve().then(() => require("./App-BfvnOBS9.js"));
677
677
  return App;
678
678
  }
679
679
  });
@@ -1,4 +1,4 @@
1
1
  "use strict";
2
- const index = require("../_chunks/index-CVCA8dDp.js");
2
+ const index = require("../_chunks/index-DkNKkHgk.js");
3
3
  require("react/jsx-runtime");
4
4
  module.exports = index.index;
@@ -1,4 +1,4 @@
1
- import { i } from "../_chunks/index-CIpGvEcJ.mjs";
1
+ import { i } from "../_chunks/index-C58A29qR.mjs";
2
2
  import "react/jsx-runtime";
3
3
  export {
4
4
  i as default
@@ -0,0 +1,7 @@
1
+ interface SyncModalProps {
2
+ isOpen: boolean;
3
+ onClose: () => void;
4
+ onSyncComplete?: () => void;
5
+ }
6
+ export declare function SyncModal({ isOpen, onClose, onSyncComplete }: SyncModalProps): import("react/jsx-runtime").JSX.Element;
7
+ export {};
@@ -30,4 +30,52 @@ export declare const embeddingsApi: {
30
30
  get: Function;
31
31
  }, query: string) => Promise<any>;
32
32
  };
33
+ export interface SyncStatus {
34
+ neonCount: number;
35
+ strapiCount: number;
36
+ inSync: boolean;
37
+ missingInStrapi: number;
38
+ missingInNeon: number;
39
+ contentDifferences: number;
40
+ }
41
+ export interface SyncResult {
42
+ success: boolean;
43
+ timestamp: string;
44
+ dryRun?: boolean;
45
+ neonCount: number;
46
+ strapiCount: number;
47
+ actions: {
48
+ created: number;
49
+ updated: number;
50
+ orphansRemoved: number;
51
+ };
52
+ details: {
53
+ created: string[];
54
+ updated: string[];
55
+ orphansRemoved: string[];
56
+ };
57
+ errors: string[];
58
+ }
59
+ export interface RecreateResult {
60
+ success: boolean;
61
+ message: string;
62
+ totalProcessed: number;
63
+ created: number;
64
+ failed: number;
65
+ errors: string[];
66
+ }
67
+ export declare const syncApi: {
68
+ getStatus: (fetchClient: {
69
+ get: Function;
70
+ }) => Promise<SyncStatus>;
71
+ syncFromNeon: (fetchClient: {
72
+ post: Function;
73
+ }, options?: {
74
+ removeOrphans?: boolean;
75
+ dryRun?: boolean;
76
+ }) => Promise<SyncResult>;
77
+ recreateAll: (fetchClient: {
78
+ post: Function;
79
+ }) => Promise<RecreateResult>;
80
+ };
33
81
  export {};