UNPKG

@rokmohar/medusa-plugin-meilisearch

Version:
287 lines (286 loc) 12.7 kB
import { jsx, jsxs } from "react/jsx-runtime"; import { useState } from "react"; import { toast, Container, Heading, Text, Badge, Button, Switch, Input } from "@medusajs/ui"; import { useQuery, useMutation } from "@tanstack/react-query"; import { defineRouteConfig } from "@medusajs/admin-sdk"; import Medusa from "@medusajs/js-sdk"; const sdk = new Medusa({ baseUrl: __BACKEND_URL__ ?? "/", auth: { type: "session" } }); const SyncPage = () => { const [semanticSearchEnabled, setSemanticSearchEnabled] = useState(false); const [semanticRatio, setSemanticRatio] = useState(0.5); const [searchQuery, setSearchQuery] = useState("jeans"); const { data: vectorStatus, isLoading: statusLoading, error: statusError, refetch: refetchStatus } = useQuery({ queryKey: ["meilisearch-vector-status"], queryFn: async () => { return sdk.client.fetch("/admin/meilisearch/vector-status"); }, retry: 2, staleTime: 3e4 // Consider data stale after 30 seconds }); const { mutate: syncData, isPending: syncPending } = useMutation({ mutationFn: async () => { return sdk.client.fetch("/admin/meilisearch/sync", { method: "POST" }); }, onSuccess: () => { toast.success("Successfully triggered data sync to Meilisearch"); }, onError: (err) => { console.error(err); toast.error("Failed to sync data to Meilisearch"); } }); const { mutate: searchProducts, isPending: searchProductsPending } = useMutation({ mutationFn: async () => { if (!searchQuery.trim()) { throw new Error("Search query cannot be empty"); } return sdk.client.fetch("/admin/meilisearch/products-hits", { method: "POST", body: { query: searchQuery.trim(), semanticSearch: semanticSearchEnabled, semanticRatio, limit: 5, offset: 0 } }); }, onSuccess: (data) => { const hybridInfo = data.hybridSearch ? ` (hybrid search with ratio ${data.semanticRatio})` : ""; toast.success(`Search successful. Found ${data.hits.length} products${hybridInfo}`); }, onError: (err) => { console.error("Search error:", err); toast.error(err.message || "Search failed"); } }); const { mutate: searchCategories, isPending: searchCategoriesPending } = useMutation({ mutationFn: async () => { if (!searchQuery.trim()) { throw new Error("Search query cannot be empty"); } return sdk.client.fetch("/admin/meilisearch/categories-hits", { method: "POST", body: { query: searchQuery.trim(), semanticSearch: semanticSearchEnabled, semanticRatio, limit: 5, offset: 0 } }); }, onSuccess: (data) => { const hybridInfo = data.hybridSearch ? ` (hybrid search with ratio ${data.semanticRatio})` : ""; toast.success(`Search successful. Found ${data.hits.length} categories${hybridInfo}`); }, onError: (err) => { console.error("Search error:", err); toast.error(err.message || "Search failed"); } }); const handleSync = () => { syncData(); }; const handleSearchProducts = () => { searchProducts(); }; const handleSearchCategories = () => { searchCategories(); }; return /* @__PURE__ */ jsx(Container, { children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-y-6", children: [ /* @__PURE__ */ jsxs("div", { children: [ /* @__PURE__ */ jsx(Heading, { level: "h1", children: "Meilisearch Configuration" }), /* @__PURE__ */ jsx(Text, { className: "text-gray-500 mt-2", children: "Manage your Meilisearch index synchronization and AI-powered semantic search settings." }) ] }), /* @__PURE__ */ jsxs("div", { className: "border border-gray-200 rounded-lg p-6", children: [ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between mb-4", children: [ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [ /* @__PURE__ */ jsx(Heading, { level: "h2", children: "AI-Powered Semantic Search" }), statusLoading ? /* @__PURE__ */ jsx(Badge, { children: "Loading..." }) : statusError ? /* @__PURE__ */ jsx(Badge, { color: "red", children: "Error" }) : (vectorStatus == null ? void 0 : vectorStatus.enabled) ? /* @__PURE__ */ jsx(Badge, { color: "green", children: "Enabled" }) : /* @__PURE__ */ jsx(Badge, { color: "grey", children: "Disabled" }) ] }), /* @__PURE__ */ jsx( Button, { variant: "secondary", size: "small", onClick: async () => { return refetchStatus(); }, isLoading: statusLoading, children: "Refresh Status" } ) ] }), statusError && /* @__PURE__ */ jsx("div", { className: "mb-4 p-3 bg-red-50 border border-red-200 rounded-md", children: /* @__PURE__ */ jsxs(Text, { className: "text-red-800 text-sm", children: [ "Failed to load vector search status: ", statusError.message ] }) }), (vectorStatus == null ? void 0 : vectorStatus.enabled) && /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-4 mb-4", children: [ /* @__PURE__ */ jsxs("div", { children: [ /* @__PURE__ */ jsx(Text, { className: "text-sm font-medium text-gray-700", children: "Provider" }), /* @__PURE__ */ jsx(Text, { className: "text-sm text-gray-600", children: vectorStatus.provider ?? "Not specified" }) ] }), /* @__PURE__ */ jsxs("div", { children: [ /* @__PURE__ */ jsx(Text, { className: "text-sm font-medium text-gray-700", children: "Model" }), /* @__PURE__ */ jsx(Text, { className: "text-sm text-gray-600", children: vectorStatus.model ?? "Not specified" }) ] }), vectorStatus.dimensions && /* @__PURE__ */ jsxs("div", { children: [ /* @__PURE__ */ jsx(Text, { className: "text-sm font-medium text-gray-700", children: "Vector Dimensions" }), /* @__PURE__ */ jsx(Text, { className: "text-sm text-gray-600", children: vectorStatus.dimensions }) ] }), /* @__PURE__ */ jsxs("div", { children: [ /* @__PURE__ */ jsx(Text, { className: "text-sm font-medium text-gray-700", children: "Embedding Fields" }), /* @__PURE__ */ jsx(Text, { className: "text-sm text-gray-600", children: vectorStatus.embeddingFields.join(", ") }) ] }) ] }), (vectorStatus == null ? void 0 : vectorStatus.enabled) && /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [ /* @__PURE__ */ jsxs("div", { children: [ /* @__PURE__ */ jsx(Text, { className: "font-medium", children: "Enable Semantic Search in Tests" }), /* @__PURE__ */ jsx(Text, { className: "text-sm text-gray-500", children: "Use AI-powered semantic search for better results" }) ] }), /* @__PURE__ */ jsx(Switch, { checked: semanticSearchEnabled, onCheckedChange: setSemanticSearchEnabled }) ] }), semanticSearchEnabled && /* @__PURE__ */ jsxs("div", { children: [ /* @__PURE__ */ jsxs(Text, { className: "font-medium mb-2", children: [ "Semantic Ratio: ", semanticRatio ] }), /* @__PURE__ */ jsx(Text, { className: "text-sm text-gray-500 mb-3", children: "0.0 = Pure keyword search, 1.0 = Pure semantic search, 0.5 = Balanced hybrid" }), /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [ /* @__PURE__ */ jsx( Input, { type: "range", min: "0", max: "1", step: "0.1", value: semanticRatio, onChange: (e) => { return setSemanticRatio(parseFloat(e.target.value)); }, className: "flex-1", "aria-label": "Semantic search ratio", "aria-describedby": "semantic-ratio-description" } ), /* @__PURE__ */ jsx("span", { className: "text-sm text-gray-700", children: semanticRatio.toFixed(1) }) ] }), /* @__PURE__ */ jsx("div", { id: "semantic-ratio-description", className: "sr-only", children: "Adjust the balance between keyword and semantic search from 0 to 1" }) ] }) ] }), !(vectorStatus == null ? void 0 : vectorStatus.enabled) && /* @__PURE__ */ jsx(Text, { className: "text-gray-500", children: "Vector search is not configured. Add vectorSearch configuration to your plugin options to enable AI-powered semantic search." }) ] }), /* @__PURE__ */ jsxs("div", { className: "border border-gray-200 rounded-lg p-6", children: [ /* @__PURE__ */ jsx("div", { className: "flex items-center gap-3 mb-4", children: /* @__PURE__ */ jsx(Heading, { level: "h2", children: "Data Synchronization" }) }), /* @__PURE__ */ jsxs(Text, { className: "text-gray-500 mb-4", children: [ "Manually trigger synchronization of your product catalog with Meilisearch.", (vectorStatus == null ? void 0 : vectorStatus.enabled) && " This will also generate embeddings for semantic search." ] }), /* @__PURE__ */ jsx(Button, { onClick: handleSync, isLoading: syncPending, variant: "primary", children: "Sync Now" }) ] }), /* @__PURE__ */ jsxs("div", { className: "border border-gray-200 rounded-lg p-6", children: [ /* @__PURE__ */ jsx("div", { className: "flex items-center gap-3 mb-4", children: /* @__PURE__ */ jsx(Heading, { level: "h2", children: "Search Testing" }) }), /* @__PURE__ */ jsxs(Text, { className: "text-gray-500 mb-4", children: [ "Test your products search configuration with a custom query.", (vectorStatus == null ? void 0 : vectorStatus.enabled) && " You can test both traditional keyword search and AI-powered semantic search." ] }), /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [ /* @__PURE__ */ jsxs("div", { children: [ /* @__PURE__ */ jsx(Text, { className: "text-sm font-medium text-gray-700 mb-2", children: "Search Query" }), /* @__PURE__ */ jsx( Input, { value: searchQuery, onChange: (e) => { return setSearchQuery(e.target.value); }, placeholder: "Enter search query (e.g., 'blue shirt', 'comfortable clothing')", className: "w-full", "aria-label": "Search query input", "aria-describedby": "search-query-help" } ), /* @__PURE__ */ jsx("div", { id: "search-query-help", className: "sr-only", children: "Enter a search term to test the search functionality" }) ] }), /* @__PURE__ */ jsxs("div", { className: "flex gap-3", children: [ /* @__PURE__ */ jsx( Button, { onClick: handleSearchProducts, isLoading: searchProductsPending, variant: "secondary", disabled: !searchQuery.trim(), children: "Search Products" } ), /* @__PURE__ */ jsx( Button, { onClick: handleSearchCategories, isLoading: searchCategoriesPending, variant: "secondary", disabled: !searchQuery.trim(), children: "Search Categories" } ), (vectorStatus == null ? void 0 : vectorStatus.enabled) && /* @__PURE__ */ jsx(Text, { className: "text-sm text-gray-500 self-center", children: semanticSearchEnabled ? `Using hybrid search (${Math.round(semanticRatio * 100)}% semantic)` : "Using keyword search only" }) ] }) ] }) ] }) ] }) }); }; const config = defineRouteConfig({ label: "Meilisearch" }); const widgetModule = { widgets: [] }; const routeModule = { routes: [ { Component: SyncPage, path: "/settings/meilisearch" } ] }; const menuItemModule = { menuItems: [ { label: config.label, icon: void 0, path: "/settings/meilisearch", nested: void 0, rank: void 0, translationNs: void 0 } ] }; const formModule = { customFields: {} }; const displayModule = { displays: {} }; const i18nModule = { resources: {} }; const plugin = { widgetModule, routeModule, menuItemModule, formModule, displayModule, i18nModule }; export { plugin as default };