strapi-content-embeddings 0.1.8 → 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.
@@ -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-DkNKkHgk.js");
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");
@@ -5,7 +5,7 @@ import { useRef, useState, useEffect, useCallback, useMemo } from "react";
5
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
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-C58A29qR.mjs";
8
+ import { P as PLUGIN_ID, R as RobotIcon, M as MarkdownEditor } from "./index-ifqYByO5.mjs";
9
9
  import styled from "styled-components";
10
10
  import ReactMarkdown from "react-markdown";
11
11
  import { useIntl } from "react-intl";
@@ -425,7 +425,7 @@ function MarkdownEditor({ content, onChange, height = 300 }) {
425
425
  )
426
426
  ] });
427
427
  }
428
- const StyledTypography = styled__default.default(designSystem.Typography)`
428
+ styled__default.default(designSystem.Typography)`
429
429
  display: block;
430
430
  margin-top: 1rem;
431
431
  margin-bottom: 0.5rem;
@@ -442,6 +442,7 @@ function EmbeddingsModal() {
442
442
  const [title, setTitle] = react.useState("");
443
443
  const [content, setContent] = react.useState("");
444
444
  const [fieldName, setFieldName] = react.useState("");
445
+ const [availableFields, setAvailableFields] = react.useState([]);
445
446
  const [isLoading, setIsLoading] = react.useState(false);
446
447
  const [isCheckingExisting, setIsCheckingExisting] = react.useState(true);
447
448
  const [existingEmbedding, setExistingEmbedding] = react.useState(null);
@@ -474,37 +475,84 @@ function EmbeddingsModal() {
474
475
  }
475
476
  checkExistingEmbedding();
476
477
  }, [id, slug, get]);
477
- const extractContentFromForm = react.useCallback(() => {
478
- if (!modifiedValues) return "";
479
- const textFieldNames = ["content", "description", "body", "text", "richtext", "markdown"];
480
- for (const name of textFieldNames) {
481
- const value = modifiedValues[name];
482
- if (value) {
483
- if (typeof value === "string" && value.trim()) {
484
- setFieldName(name);
485
- return value;
486
- } else if (Array.isArray(value)) {
487
- const text = value.map((block) => {
488
- if (block.children) {
489
- return block.children.map((child) => child.text || "").join("");
490
- }
491
- return "";
492
- }).join("\n\n");
493
- if (text.trim()) {
494
- setFieldName(name);
495
- return text;
478
+ const extractTextFromField = react.useCallback((value, depth = 0) => {
479
+ if (!value || depth > 5) return "";
480
+ if (typeof value === "string") {
481
+ return value.trim();
482
+ }
483
+ if (Array.isArray(value)) {
484
+ const texts = [];
485
+ for (const item of value) {
486
+ if (item && typeof item === "object" && item.__component) {
487
+ for (const [key, fieldValue] of Object.entries(item)) {
488
+ if (key === "__component" || key === "id") continue;
489
+ const extracted = extractTextFromField(fieldValue, depth + 1);
490
+ if (extracted) texts.push(extracted);
496
491
  }
492
+ } else if (item && item.children) {
493
+ const blockText = item.children.map((child) => child.text || "").join("");
494
+ if (blockText) texts.push(blockText);
495
+ } else if (item && typeof item === "object") {
496
+ const extracted = extractTextFromField(item, depth + 1);
497
+ if (extracted) texts.push(extracted);
497
498
  }
498
499
  }
500
+ return texts.join("\n\n").trim();
501
+ }
502
+ if (typeof value === "object") {
503
+ const texts = [];
504
+ for (const [key, fieldValue] of Object.entries(value)) {
505
+ if (["id", "__component", "documentId", "createdAt", "updatedAt"].includes(key)) continue;
506
+ const extracted = extractTextFromField(fieldValue, depth + 1);
507
+ if (extracted) texts.push(extracted);
508
+ }
509
+ return texts.join("\n\n").trim();
499
510
  }
500
511
  return "";
501
- }, [modifiedValues]);
512
+ }, []);
513
+ const isDynamicZone = (value) => {
514
+ return Array.isArray(value) && value.length > 0 && value[0]?.__component;
515
+ };
516
+ const detectTextFields = react.useCallback(() => {
517
+ if (!modifiedValues) return [];
518
+ const fields = [];
519
+ for (const [name, value] of Object.entries(modifiedValues)) {
520
+ if (["id", "documentId", "createdAt", "updatedAt", "publishedAt", "locale", "localizations"].includes(name)) {
521
+ continue;
522
+ }
523
+ const textValue = extractTextFromField(value);
524
+ if (textValue && textValue.length > 0) {
525
+ let label = name.replace(/([A-Z])/g, " $1").replace(/^./, (str) => str.toUpperCase()).trim();
526
+ if (isDynamicZone(value)) {
527
+ const componentCount = value.length;
528
+ label += ` (${componentCount} component${componentCount > 1 ? "s" : ""})`;
529
+ }
530
+ fields.push({
531
+ name,
532
+ label,
533
+ value: textValue,
534
+ charCount: textValue.length
535
+ });
536
+ }
537
+ }
538
+ fields.sort((a, b) => b.charCount - a.charCount);
539
+ return fields;
540
+ }, [modifiedValues, extractTextFromField]);
502
541
  react.useEffect(() => {
503
- const formContent = extractContentFromForm();
504
- if (formContent) {
505
- setContent(formContent);
542
+ const fields = detectTextFields();
543
+ setAvailableFields(fields);
544
+ if (fields.length > 0 && !fieldName) {
545
+ setFieldName(fields[0].name);
546
+ setContent(fields[0].value);
506
547
  }
507
- }, [extractContentFromForm]);
548
+ }, [detectTextFields, fieldName]);
549
+ const handleFieldChange = (selectedFieldName) => {
550
+ setFieldName(selectedFieldName);
551
+ const selectedField = availableFields.find((f) => f.name === selectedFieldName);
552
+ if (selectedField) {
553
+ setContent(selectedField.value);
554
+ }
555
+ };
508
556
  const contentLength = content.length;
509
557
  const willChunk = contentLength > CHUNK_SIZE;
510
558
  const estimatedChunks = willChunk ? Math.ceil(contentLength / (CHUNK_SIZE - 200)) : 1;
@@ -521,9 +569,11 @@ function EmbeddingsModal() {
521
569
  const isValid = title.trim() && content.trim();
522
570
  function handleOpenCreate() {
523
571
  setTitle("");
524
- const formContent = extractContentFromForm();
525
- if (formContent) {
526
- setContent(formContent);
572
+ const fields = detectTextFields();
573
+ setAvailableFields(fields);
574
+ if (fields.length > 0) {
575
+ setFieldName(fields[0].name);
576
+ setContent(fields[0].value);
527
577
  }
528
578
  setIsVisible(true);
529
579
  }
@@ -609,16 +659,6 @@ function EmbeddingsModal() {
609
659
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Modal.Root, { open: isVisible, onOpenChange: setIsVisible, children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Modal.Content, { children: [
610
660
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Modal.Header, { children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Modal.Title, { children: "Create Embedding from Content" }) }),
611
661
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Modal.Body, { children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Box, { children: [
612
- /* @__PURE__ */ jsxRuntime.jsxs(StyledTypography, { variant: "omega", textColor: "neutral600", children: [
613
- "Content: ",
614
- contentLength,
615
- " characters",
616
- willChunk && /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Typography, { textColor: "primary600", children: [
617
- " (will create ~",
618
- estimatedChunks,
619
- " embeddings)"
620
- ] })
621
- ] }),
622
662
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { marginBottom: 4, children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Field.Root, { children: [
623
663
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: "Title" }),
624
664
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -630,10 +670,38 @@ function EmbeddingsModal() {
630
670
  }
631
671
  )
632
672
  ] }) }),
633
- /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Box, { marginBottom: 4, children: [
673
+ availableFields.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Box, { marginBottom: 4, children: [
634
674
  /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Field.Root, { children: [
635
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: "Content" }),
636
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Hint, { children: fieldName ? `From field: ${fieldName}` : "Enter content manually" })
675
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: "Source Field" }),
676
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Hint, { children: "Select which field to use for the embedding content" })
677
+ ] }),
678
+ /* @__PURE__ */ jsxRuntime.jsx(
679
+ designSystem.SingleSelect,
680
+ {
681
+ value: fieldName,
682
+ onChange: (value) => handleFieldChange(value),
683
+ placeholder: "Select a field",
684
+ children: availableFields.map((field) => /* @__PURE__ */ jsxRuntime.jsxs(designSystem.SingleSelectOption, { value: field.name, children: [
685
+ field.label,
686
+ " (",
687
+ field.charCount.toLocaleString(),
688
+ " chars)"
689
+ ] }, field.name))
690
+ }
691
+ )
692
+ ] }),
693
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Box, { marginBottom: 4, children: [
694
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { justifyContent: "space-between", alignItems: "center", children: [
695
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Root, { children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: "Content Preview" }) }),
696
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Typography, { variant: "pi", textColor: "neutral600", children: [
697
+ contentLength.toLocaleString(),
698
+ " characters",
699
+ willChunk && /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Typography, { textColor: "primary600", children: [
700
+ " (~",
701
+ estimatedChunks,
702
+ " chunks)"
703
+ ] })
704
+ ] })
637
705
  ] }),
638
706
  /* @__PURE__ */ jsxRuntime.jsx(
639
707
  MarkdownEditor,
@@ -673,7 +741,7 @@ const index = {
673
741
  defaultMessage: PLUGIN_ID
674
742
  },
675
743
  Component: async () => {
676
- const { App } = await Promise.resolve().then(() => require("./App-BfvnOBS9.js"));
744
+ const { App } = await Promise.resolve().then(() => require("./App-ByRBbkZn.js"));
677
745
  return App;
678
746
  }
679
747
  });
@@ -1,6 +1,6 @@
1
1
  import { jsx, jsxs, Fragment } from "react/jsx-runtime";
2
2
  import { useRef, useEffect, useState, useCallback } from "react";
3
- import { Box, Typography, Loader, Button, Modal, Field, TextInput } from "@strapi/design-system";
3
+ import { Box, Typography, Loader, Button, Modal, Field, TextInput, SingleSelect, SingleSelectOption, Flex } from "@strapi/design-system";
4
4
  import { useNavigate } from "react-router-dom";
5
5
  import styled, { createGlobalStyle } from "styled-components";
6
6
  import qs from "qs";
@@ -421,7 +421,7 @@ function MarkdownEditor({ content, onChange, height = 300 }) {
421
421
  )
422
422
  ] });
423
423
  }
424
- const StyledTypography = styled(Typography)`
424
+ styled(Typography)`
425
425
  display: block;
426
426
  margin-top: 1rem;
427
427
  margin-bottom: 0.5rem;
@@ -438,6 +438,7 @@ function EmbeddingsModal() {
438
438
  const [title, setTitle] = useState("");
439
439
  const [content, setContent] = useState("");
440
440
  const [fieldName, setFieldName] = useState("");
441
+ const [availableFields, setAvailableFields] = useState([]);
441
442
  const [isLoading, setIsLoading] = useState(false);
442
443
  const [isCheckingExisting, setIsCheckingExisting] = useState(true);
443
444
  const [existingEmbedding, setExistingEmbedding] = useState(null);
@@ -470,37 +471,84 @@ function EmbeddingsModal() {
470
471
  }
471
472
  checkExistingEmbedding();
472
473
  }, [id, slug, get]);
473
- const extractContentFromForm = useCallback(() => {
474
- if (!modifiedValues) return "";
475
- const textFieldNames = ["content", "description", "body", "text", "richtext", "markdown"];
476
- for (const name of textFieldNames) {
477
- const value = modifiedValues[name];
478
- if (value) {
479
- if (typeof value === "string" && value.trim()) {
480
- setFieldName(name);
481
- return value;
482
- } else if (Array.isArray(value)) {
483
- const text = value.map((block) => {
484
- if (block.children) {
485
- return block.children.map((child) => child.text || "").join("");
486
- }
487
- return "";
488
- }).join("\n\n");
489
- if (text.trim()) {
490
- setFieldName(name);
491
- return text;
474
+ const extractTextFromField = useCallback((value, depth = 0) => {
475
+ if (!value || depth > 5) return "";
476
+ if (typeof value === "string") {
477
+ return value.trim();
478
+ }
479
+ if (Array.isArray(value)) {
480
+ const texts = [];
481
+ for (const item of value) {
482
+ if (item && typeof item === "object" && item.__component) {
483
+ for (const [key, fieldValue] of Object.entries(item)) {
484
+ if (key === "__component" || key === "id") continue;
485
+ const extracted = extractTextFromField(fieldValue, depth + 1);
486
+ if (extracted) texts.push(extracted);
492
487
  }
488
+ } else if (item && item.children) {
489
+ const blockText = item.children.map((child) => child.text || "").join("");
490
+ if (blockText) texts.push(blockText);
491
+ } else if (item && typeof item === "object") {
492
+ const extracted = extractTextFromField(item, depth + 1);
493
+ if (extracted) texts.push(extracted);
493
494
  }
494
495
  }
496
+ return texts.join("\n\n").trim();
497
+ }
498
+ if (typeof value === "object") {
499
+ const texts = [];
500
+ for (const [key, fieldValue] of Object.entries(value)) {
501
+ if (["id", "__component", "documentId", "createdAt", "updatedAt"].includes(key)) continue;
502
+ const extracted = extractTextFromField(fieldValue, depth + 1);
503
+ if (extracted) texts.push(extracted);
504
+ }
505
+ return texts.join("\n\n").trim();
495
506
  }
496
507
  return "";
497
- }, [modifiedValues]);
508
+ }, []);
509
+ const isDynamicZone = (value) => {
510
+ return Array.isArray(value) && value.length > 0 && value[0]?.__component;
511
+ };
512
+ const detectTextFields = useCallback(() => {
513
+ if (!modifiedValues) return [];
514
+ const fields = [];
515
+ for (const [name, value] of Object.entries(modifiedValues)) {
516
+ if (["id", "documentId", "createdAt", "updatedAt", "publishedAt", "locale", "localizations"].includes(name)) {
517
+ continue;
518
+ }
519
+ const textValue = extractTextFromField(value);
520
+ if (textValue && textValue.length > 0) {
521
+ let label = name.replace(/([A-Z])/g, " $1").replace(/^./, (str) => str.toUpperCase()).trim();
522
+ if (isDynamicZone(value)) {
523
+ const componentCount = value.length;
524
+ label += ` (${componentCount} component${componentCount > 1 ? "s" : ""})`;
525
+ }
526
+ fields.push({
527
+ name,
528
+ label,
529
+ value: textValue,
530
+ charCount: textValue.length
531
+ });
532
+ }
533
+ }
534
+ fields.sort((a, b) => b.charCount - a.charCount);
535
+ return fields;
536
+ }, [modifiedValues, extractTextFromField]);
498
537
  useEffect(() => {
499
- const formContent = extractContentFromForm();
500
- if (formContent) {
501
- setContent(formContent);
538
+ const fields = detectTextFields();
539
+ setAvailableFields(fields);
540
+ if (fields.length > 0 && !fieldName) {
541
+ setFieldName(fields[0].name);
542
+ setContent(fields[0].value);
502
543
  }
503
- }, [extractContentFromForm]);
544
+ }, [detectTextFields, fieldName]);
545
+ const handleFieldChange = (selectedFieldName) => {
546
+ setFieldName(selectedFieldName);
547
+ const selectedField = availableFields.find((f) => f.name === selectedFieldName);
548
+ if (selectedField) {
549
+ setContent(selectedField.value);
550
+ }
551
+ };
504
552
  const contentLength = content.length;
505
553
  const willChunk = contentLength > CHUNK_SIZE;
506
554
  const estimatedChunks = willChunk ? Math.ceil(contentLength / (CHUNK_SIZE - 200)) : 1;
@@ -517,9 +565,11 @@ function EmbeddingsModal() {
517
565
  const isValid = title.trim() && content.trim();
518
566
  function handleOpenCreate() {
519
567
  setTitle("");
520
- const formContent = extractContentFromForm();
521
- if (formContent) {
522
- setContent(formContent);
568
+ const fields = detectTextFields();
569
+ setAvailableFields(fields);
570
+ if (fields.length > 0) {
571
+ setFieldName(fields[0].name);
572
+ setContent(fields[0].value);
523
573
  }
524
574
  setIsVisible(true);
525
575
  }
@@ -605,16 +655,6 @@ function EmbeddingsModal() {
605
655
  /* @__PURE__ */ jsx(Modal.Root, { open: isVisible, onOpenChange: setIsVisible, children: /* @__PURE__ */ jsxs(Modal.Content, { children: [
606
656
  /* @__PURE__ */ jsx(Modal.Header, { children: /* @__PURE__ */ jsx(Modal.Title, { children: "Create Embedding from Content" }) }),
607
657
  /* @__PURE__ */ jsx(Modal.Body, { children: /* @__PURE__ */ jsxs(Box, { children: [
608
- /* @__PURE__ */ jsxs(StyledTypography, { variant: "omega", textColor: "neutral600", children: [
609
- "Content: ",
610
- contentLength,
611
- " characters",
612
- willChunk && /* @__PURE__ */ jsxs(Typography, { textColor: "primary600", children: [
613
- " (will create ~",
614
- estimatedChunks,
615
- " embeddings)"
616
- ] })
617
- ] }),
618
658
  /* @__PURE__ */ jsx(Box, { marginBottom: 4, children: /* @__PURE__ */ jsxs(Field.Root, { children: [
619
659
  /* @__PURE__ */ jsx(Field.Label, { children: "Title" }),
620
660
  /* @__PURE__ */ jsx(
@@ -626,10 +666,38 @@ function EmbeddingsModal() {
626
666
  }
627
667
  )
628
668
  ] }) }),
629
- /* @__PURE__ */ jsxs(Box, { marginBottom: 4, children: [
669
+ availableFields.length > 0 && /* @__PURE__ */ jsxs(Box, { marginBottom: 4, children: [
630
670
  /* @__PURE__ */ jsxs(Field.Root, { children: [
631
- /* @__PURE__ */ jsx(Field.Label, { children: "Content" }),
632
- /* @__PURE__ */ jsx(Field.Hint, { children: fieldName ? `From field: ${fieldName}` : "Enter content manually" })
671
+ /* @__PURE__ */ jsx(Field.Label, { children: "Source Field" }),
672
+ /* @__PURE__ */ jsx(Field.Hint, { children: "Select which field to use for the embedding content" })
673
+ ] }),
674
+ /* @__PURE__ */ jsx(
675
+ SingleSelect,
676
+ {
677
+ value: fieldName,
678
+ onChange: (value) => handleFieldChange(value),
679
+ placeholder: "Select a field",
680
+ children: availableFields.map((field) => /* @__PURE__ */ jsxs(SingleSelectOption, { value: field.name, children: [
681
+ field.label,
682
+ " (",
683
+ field.charCount.toLocaleString(),
684
+ " chars)"
685
+ ] }, field.name))
686
+ }
687
+ )
688
+ ] }),
689
+ /* @__PURE__ */ jsxs(Box, { marginBottom: 4, children: [
690
+ /* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "center", children: [
691
+ /* @__PURE__ */ jsx(Field.Root, { children: /* @__PURE__ */ jsx(Field.Label, { children: "Content Preview" }) }),
692
+ /* @__PURE__ */ jsxs(Typography, { variant: "pi", textColor: "neutral600", children: [
693
+ contentLength.toLocaleString(),
694
+ " characters",
695
+ willChunk && /* @__PURE__ */ jsxs(Typography, { textColor: "primary600", children: [
696
+ " (~",
697
+ estimatedChunks,
698
+ " chunks)"
699
+ ] })
700
+ ] })
633
701
  ] }),
634
702
  /* @__PURE__ */ jsx(
635
703
  MarkdownEditor,
@@ -669,7 +737,7 @@ const index = {
669
737
  defaultMessage: PLUGIN_ID
670
738
  },
671
739
  Component: async () => {
672
- const { App } = await import("./App-sRU0Nh3x.mjs");
740
+ const { App } = await import("./App-MjsTrWRS.mjs");
673
741
  return App;
674
742
  }
675
743
  });
@@ -1,4 +1,4 @@
1
1
  "use strict";
2
- const index = require("../_chunks/index-DkNKkHgk.js");
2
+ const index = require("../_chunks/index-TWbcT-zJ.js");
3
3
  require("react/jsx-runtime");
4
4
  module.exports = index.index;
@@ -1,4 +1,4 @@
1
- import { i } from "../_chunks/index-C58A29qR.mjs";
1
+ import { i } from "../_chunks/index-ifqYByO5.mjs";
2
2
  import "react/jsx-runtime";
3
3
  export {
4
4
  i as default
@@ -2070,12 +2070,12 @@ const embeddings = ({ strapi }) => ({
2070
2070
  return chunks.length;
2071
2071
  },
2072
2072
  /**
2073
- * Update embeddings with automatic chunking support
2074
- * Handles re-chunking when content changes and exceeds chunk size
2073
+ * Update a single chunk's content and embedding
2074
+ * Updates only the specified chunk without affecting other chunks in the group
2075
2075
  */
2076
2076
  async updateChunkedEmbedding(id, data) {
2077
- const { title, content, metadata, autoChunk } = data.data;
2078
- const config2 = this.getConfig();
2077
+ const { title, content, metadata } = data.data;
2078
+ this.getConfig();
2079
2079
  const currentEntry = await strapi.documents(CONTENT_TYPE_UID$1).findOne({
2080
2080
  documentId: id
2081
2081
  });
@@ -2083,64 +2083,61 @@ const embeddings = ({ strapi }) => ({
2083
2083
  throw new Error(`Embedding with id ${id} not found`);
2084
2084
  }
2085
2085
  const currentMetadata = currentEntry.metadata;
2086
- const parentDocumentId = currentMetadata?.parentDocumentId || id;
2087
- const newContent = content ?? currentEntry.content;
2088
- const newTitle = title ?? currentMetadata?.originalTitle ?? currentEntry.title;
2089
- const shouldChunk = autoChunk ?? config2.autoChunk;
2090
- const chunkSize = config2.chunkSize || 4e3;
2091
- const contentNeedsChunking = shouldChunk && needsChunking(newContent, chunkSize);
2092
- const existingChunks = await this.findRelatedChunks(id);
2093
- let originalRelated;
2094
- const firstChunk = existingChunks.find(
2095
- (c) => c.metadata?.chunkIndex === 0 || c.documentId === parentDocumentId
2096
- );
2097
- if (firstChunk?.related) {
2098
- originalRelated = firstChunk.related;
2099
- }
2100
- const deletedCount = await this.deleteRelatedChunks(id);
2101
- console.log(`Deleted ${deletedCount} existing chunk(s) for update`);
2102
- const preservedMetadata = { ...metadata };
2103
- delete preservedMetadata?.isChunk;
2104
- delete preservedMetadata?.chunkIndex;
2105
- delete preservedMetadata?.totalChunks;
2106
- delete preservedMetadata?.startOffset;
2107
- delete preservedMetadata?.endOffset;
2108
- delete preservedMetadata?.originalTitle;
2109
- delete preservedMetadata?.parentDocumentId;
2110
- delete preservedMetadata?.estimatedTokens;
2111
- if (contentNeedsChunking) {
2112
- return await this.createChunkedEmbedding({
2113
- data: {
2114
- title: newTitle.replace(/\s*\[Part \d+\/\d+\]$/, ""),
2115
- // Remove old part suffix
2116
- content: newContent,
2117
- collectionType: currentEntry.collectionType || "standalone",
2118
- fieldName: currentEntry.fieldName || "content",
2119
- metadata: preservedMetadata,
2120
- related: originalRelated,
2121
- autoChunk: true
2122
- }
2123
- });
2124
- } else {
2125
- const entity = await this.createEmbedding({
2126
- data: {
2127
- title: newTitle.replace(/\s*\[Part \d+\/\d+\]$/, ""),
2128
- // Remove old part suffix
2086
+ const contentChanged = content !== void 0 && content !== currentEntry.content;
2087
+ console.log(`[updateChunkedEmbedding] Updating single chunk ${id}, contentChanged: ${contentChanged}`);
2088
+ const updateData = {};
2089
+ if (title !== void 0) {
2090
+ const currentTitle = currentEntry.title || "";
2091
+ const partMatch = currentTitle.match(/\s*\[Part \d+\/\d+\]$/);
2092
+ updateData.title = partMatch ? `${title}${partMatch[0]}` : title;
2093
+ }
2094
+ if (content !== void 0) {
2095
+ updateData.content = content;
2096
+ }
2097
+ if (metadata !== void 0) {
2098
+ updateData.metadata = {
2099
+ ...currentMetadata,
2100
+ ...metadata
2101
+ };
2102
+ if (contentChanged) {
2103
+ updateData.metadata.estimatedTokens = estimateTokens(updateData.content || currentEntry.content);
2104
+ }
2105
+ }
2106
+ const updatedEntity = await strapi.documents(CONTENT_TYPE_UID$1).update({
2107
+ documentId: id,
2108
+ data: updateData
2109
+ });
2110
+ if (pluginManager.isInitialized() && (contentChanged || title !== void 0)) {
2111
+ try {
2112
+ console.log(`[updateChunkedEmbedding] Updating embedding in Neon for chunk ${id}`);
2113
+ await pluginManager.deleteEmbedding(id);
2114
+ const newContent = updateData.content || currentEntry.content;
2115
+ const result = await pluginManager.createEmbedding({
2116
+ id,
2117
+ title: updatedEntity.title || currentEntry.title,
2129
2118
  content: newContent,
2130
2119
  collectionType: currentEntry.collectionType || "standalone",
2131
- fieldName: currentEntry.fieldName || "content",
2132
- metadata: preservedMetadata,
2133
- related: originalRelated,
2134
- autoChunk: false
2135
- }
2136
- });
2137
- return {
2138
- entity,
2139
- chunks: [entity],
2140
- totalChunks: 1,
2141
- wasChunked: false
2142
- };
2120
+ fieldName: currentEntry.fieldName || "content"
2121
+ });
2122
+ await strapi.documents(CONTENT_TYPE_UID$1).update({
2123
+ documentId: id,
2124
+ data: {
2125
+ embeddingId: result.embeddingId,
2126
+ embedding: result.embedding
2127
+ }
2128
+ });
2129
+ console.log(`[updateChunkedEmbedding] Successfully updated embedding for chunk ${id}`);
2130
+ } catch (error) {
2131
+ console.error(`Failed to update vector store for ${id}:`, error);
2132
+ }
2143
2133
  }
2134
+ const allChunks = await this.findRelatedChunks(id);
2135
+ return {
2136
+ entity: updatedEntity,
2137
+ chunks: allChunks,
2138
+ totalChunks: allChunks.length,
2139
+ wasChunked: allChunks.length > 1
2140
+ };
2144
2141
  },
2145
2142
  async updateEmbedding(id, data) {
2146
2143
  const { title, content: rawContent, metadata, autoChunk } = data.data;
@@ -2067,12 +2067,12 @@ const embeddings = ({ strapi }) => ({
2067
2067
  return chunks.length;
2068
2068
  },
2069
2069
  /**
2070
- * Update embeddings with automatic chunking support
2071
- * Handles re-chunking when content changes and exceeds chunk size
2070
+ * Update a single chunk's content and embedding
2071
+ * Updates only the specified chunk without affecting other chunks in the group
2072
2072
  */
2073
2073
  async updateChunkedEmbedding(id, data) {
2074
- const { title, content, metadata, autoChunk } = data.data;
2075
- const config2 = this.getConfig();
2074
+ const { title, content, metadata } = data.data;
2075
+ this.getConfig();
2076
2076
  const currentEntry = await strapi.documents(CONTENT_TYPE_UID$1).findOne({
2077
2077
  documentId: id
2078
2078
  });
@@ -2080,64 +2080,61 @@ const embeddings = ({ strapi }) => ({
2080
2080
  throw new Error(`Embedding with id ${id} not found`);
2081
2081
  }
2082
2082
  const currentMetadata = currentEntry.metadata;
2083
- const parentDocumentId = currentMetadata?.parentDocumentId || id;
2084
- const newContent = content ?? currentEntry.content;
2085
- const newTitle = title ?? currentMetadata?.originalTitle ?? currentEntry.title;
2086
- const shouldChunk = autoChunk ?? config2.autoChunk;
2087
- const chunkSize = config2.chunkSize || 4e3;
2088
- const contentNeedsChunking = shouldChunk && needsChunking(newContent, chunkSize);
2089
- const existingChunks = await this.findRelatedChunks(id);
2090
- let originalRelated;
2091
- const firstChunk = existingChunks.find(
2092
- (c) => c.metadata?.chunkIndex === 0 || c.documentId === parentDocumentId
2093
- );
2094
- if (firstChunk?.related) {
2095
- originalRelated = firstChunk.related;
2096
- }
2097
- const deletedCount = await this.deleteRelatedChunks(id);
2098
- console.log(`Deleted ${deletedCount} existing chunk(s) for update`);
2099
- const preservedMetadata = { ...metadata };
2100
- delete preservedMetadata?.isChunk;
2101
- delete preservedMetadata?.chunkIndex;
2102
- delete preservedMetadata?.totalChunks;
2103
- delete preservedMetadata?.startOffset;
2104
- delete preservedMetadata?.endOffset;
2105
- delete preservedMetadata?.originalTitle;
2106
- delete preservedMetadata?.parentDocumentId;
2107
- delete preservedMetadata?.estimatedTokens;
2108
- if (contentNeedsChunking) {
2109
- return await this.createChunkedEmbedding({
2110
- data: {
2111
- title: newTitle.replace(/\s*\[Part \d+\/\d+\]$/, ""),
2112
- // Remove old part suffix
2113
- content: newContent,
2114
- collectionType: currentEntry.collectionType || "standalone",
2115
- fieldName: currentEntry.fieldName || "content",
2116
- metadata: preservedMetadata,
2117
- related: originalRelated,
2118
- autoChunk: true
2119
- }
2120
- });
2121
- } else {
2122
- const entity = await this.createEmbedding({
2123
- data: {
2124
- title: newTitle.replace(/\s*\[Part \d+\/\d+\]$/, ""),
2125
- // Remove old part suffix
2083
+ const contentChanged = content !== void 0 && content !== currentEntry.content;
2084
+ console.log(`[updateChunkedEmbedding] Updating single chunk ${id}, contentChanged: ${contentChanged}`);
2085
+ const updateData = {};
2086
+ if (title !== void 0) {
2087
+ const currentTitle = currentEntry.title || "";
2088
+ const partMatch = currentTitle.match(/\s*\[Part \d+\/\d+\]$/);
2089
+ updateData.title = partMatch ? `${title}${partMatch[0]}` : title;
2090
+ }
2091
+ if (content !== void 0) {
2092
+ updateData.content = content;
2093
+ }
2094
+ if (metadata !== void 0) {
2095
+ updateData.metadata = {
2096
+ ...currentMetadata,
2097
+ ...metadata
2098
+ };
2099
+ if (contentChanged) {
2100
+ updateData.metadata.estimatedTokens = estimateTokens(updateData.content || currentEntry.content);
2101
+ }
2102
+ }
2103
+ const updatedEntity = await strapi.documents(CONTENT_TYPE_UID$1).update({
2104
+ documentId: id,
2105
+ data: updateData
2106
+ });
2107
+ if (pluginManager.isInitialized() && (contentChanged || title !== void 0)) {
2108
+ try {
2109
+ console.log(`[updateChunkedEmbedding] Updating embedding in Neon for chunk ${id}`);
2110
+ await pluginManager.deleteEmbedding(id);
2111
+ const newContent = updateData.content || currentEntry.content;
2112
+ const result = await pluginManager.createEmbedding({
2113
+ id,
2114
+ title: updatedEntity.title || currentEntry.title,
2126
2115
  content: newContent,
2127
2116
  collectionType: currentEntry.collectionType || "standalone",
2128
- fieldName: currentEntry.fieldName || "content",
2129
- metadata: preservedMetadata,
2130
- related: originalRelated,
2131
- autoChunk: false
2132
- }
2133
- });
2134
- return {
2135
- entity,
2136
- chunks: [entity],
2137
- totalChunks: 1,
2138
- wasChunked: false
2139
- };
2117
+ fieldName: currentEntry.fieldName || "content"
2118
+ });
2119
+ await strapi.documents(CONTENT_TYPE_UID$1).update({
2120
+ documentId: id,
2121
+ data: {
2122
+ embeddingId: result.embeddingId,
2123
+ embedding: result.embedding
2124
+ }
2125
+ });
2126
+ console.log(`[updateChunkedEmbedding] Successfully updated embedding for chunk ${id}`);
2127
+ } catch (error) {
2128
+ console.error(`Failed to update vector store for ${id}:`, error);
2129
+ }
2140
2130
  }
2131
+ const allChunks = await this.findRelatedChunks(id);
2132
+ return {
2133
+ entity: updatedEntity,
2134
+ chunks: allChunks,
2135
+ totalChunks: allChunks.length,
2136
+ wasChunked: allChunks.length > 1
2137
+ };
2141
2138
  },
2142
2139
  async updateEmbedding(id, data) {
2143
2140
  const { title, content: rawContent, metadata, autoChunk } = data.data;
@@ -64,8 +64,8 @@ declare const embeddings: ({ strapi }: {
64
64
  */
65
65
  deleteRelatedChunks(documentId: string): Promise<number>;
66
66
  /**
67
- * Update embeddings with automatic chunking support
68
- * Handles re-chunking when content changes and exceeds chunk size
67
+ * Update a single chunk's content and embedding
68
+ * Updates only the specified chunk without affecting other chunks in the group
69
69
  */
70
70
  updateChunkedEmbedding(id: string, data: UpdateEmbeddingData): Promise<ChunkedEmbeddingResult>;
71
71
  updateEmbedding(id: string, data: UpdateEmbeddingData): Promise<any>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "strapi-content-embeddings",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "Strapi v5 plugin for vector embeddings with OpenAI and Neon PostgreSQL. Enables semantic search, RAG chat, and MCP (Model Context Protocol) integration.",
5
5
  "keywords": [
6
6
  "strapi",