strapi-content-embeddings 0.2.1 → 0.2.2

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.
@@ -411,7 +411,7 @@ function MarkdownEditor({ content, onChange, height = 300 }) {
411
411
  )
412
412
  ] });
413
413
  }
414
- const StyledTypography = styled__default.default(designSystem.Typography)`
414
+ styled__default.default(designSystem.Typography)`
415
415
  display: block;
416
416
  margin-top: 1rem;
417
417
  margin-bottom: 0.5rem;
@@ -428,6 +428,7 @@ function EmbeddingsModal() {
428
428
  const [title, setTitle] = react.useState("");
429
429
  const [content, setContent] = react.useState("");
430
430
  const [fieldName, setFieldName] = react.useState("");
431
+ const [availableFields, setAvailableFields] = react.useState([]);
431
432
  const [isLoading, setIsLoading] = react.useState(false);
432
433
  const [isCheckingExisting, setIsCheckingExisting] = react.useState(true);
433
434
  const [existingEmbedding, setExistingEmbedding] = react.useState(null);
@@ -460,37 +461,84 @@ function EmbeddingsModal() {
460
461
  }
461
462
  checkExistingEmbedding();
462
463
  }, [id, slug, get]);
463
- const extractContentFromForm = react.useCallback(() => {
464
- if (!modifiedValues) return "";
465
- const textFieldNames = ["content", "description", "body", "text", "richtext", "markdown"];
466
- for (const name of textFieldNames) {
467
- const value = modifiedValues[name];
468
- if (value) {
469
- if (typeof value === "string" && value.trim()) {
470
- setFieldName(name);
471
- return value;
472
- } else if (Array.isArray(value)) {
473
- const text = value.map((block) => {
474
- if (block.children) {
475
- return block.children.map((child) => child.text || "").join("");
476
- }
477
- return "";
478
- }).join("\n\n");
479
- if (text.trim()) {
480
- setFieldName(name);
481
- return text;
464
+ const extractTextFromField = react.useCallback((value, depth = 0) => {
465
+ if (!value || depth > 5) return "";
466
+ if (typeof value === "string") {
467
+ return value.trim();
468
+ }
469
+ if (Array.isArray(value)) {
470
+ const texts = [];
471
+ for (const item of value) {
472
+ if (item && typeof item === "object" && item.__component) {
473
+ for (const [key, fieldValue] of Object.entries(item)) {
474
+ if (key === "__component" || key === "id") continue;
475
+ const extracted = extractTextFromField(fieldValue, depth + 1);
476
+ if (extracted) texts.push(extracted);
482
477
  }
478
+ } else if (item && item.children) {
479
+ const blockText = item.children.map((child) => child.text || "").join("");
480
+ if (blockText) texts.push(blockText);
481
+ } else if (item && typeof item === "object") {
482
+ const extracted = extractTextFromField(item, depth + 1);
483
+ if (extracted) texts.push(extracted);
483
484
  }
484
485
  }
486
+ return texts.join("\n\n").trim();
487
+ }
488
+ if (typeof value === "object") {
489
+ const texts = [];
490
+ for (const [key, fieldValue] of Object.entries(value)) {
491
+ if (["id", "__component", "documentId", "createdAt", "updatedAt"].includes(key)) continue;
492
+ const extracted = extractTextFromField(fieldValue, depth + 1);
493
+ if (extracted) texts.push(extracted);
494
+ }
495
+ return texts.join("\n\n").trim();
485
496
  }
486
497
  return "";
487
- }, [modifiedValues]);
498
+ }, []);
499
+ const isDynamicZone = (value) => {
500
+ return Array.isArray(value) && value.length > 0 && value[0]?.__component;
501
+ };
502
+ const detectTextFields = react.useCallback(() => {
503
+ if (!modifiedValues) return [];
504
+ const fields = [];
505
+ for (const [name, value] of Object.entries(modifiedValues)) {
506
+ if (["id", "documentId", "createdAt", "updatedAt", "publishedAt", "locale", "localizations"].includes(name)) {
507
+ continue;
508
+ }
509
+ const textValue = extractTextFromField(value);
510
+ if (textValue && textValue.length > 0) {
511
+ let label = name.replace(/([A-Z])/g, " $1").replace(/^./, (str) => str.toUpperCase()).trim();
512
+ if (isDynamicZone(value)) {
513
+ const componentCount = value.length;
514
+ label += ` (${componentCount} component${componentCount > 1 ? "s" : ""})`;
515
+ }
516
+ fields.push({
517
+ name,
518
+ label,
519
+ value: textValue,
520
+ charCount: textValue.length
521
+ });
522
+ }
523
+ }
524
+ fields.sort((a, b) => b.charCount - a.charCount);
525
+ return fields;
526
+ }, [modifiedValues, extractTextFromField]);
488
527
  react.useEffect(() => {
489
- const formContent = extractContentFromForm();
490
- if (formContent) {
491
- setContent(formContent);
528
+ const fields = detectTextFields();
529
+ setAvailableFields(fields);
530
+ if (fields.length > 0 && !fieldName) {
531
+ setFieldName(fields[0].name);
532
+ setContent(fields[0].value);
492
533
  }
493
- }, [extractContentFromForm]);
534
+ }, [detectTextFields, fieldName]);
535
+ const handleFieldChange = (selectedFieldName) => {
536
+ setFieldName(selectedFieldName);
537
+ const selectedField = availableFields.find((f) => f.name === selectedFieldName);
538
+ if (selectedField) {
539
+ setContent(selectedField.value);
540
+ }
541
+ };
494
542
  const contentLength = content.length;
495
543
  const willChunk = contentLength > CHUNK_SIZE;
496
544
  const estimatedChunks = willChunk ? Math.ceil(contentLength / (CHUNK_SIZE - 200)) : 1;
@@ -507,9 +555,11 @@ function EmbeddingsModal() {
507
555
  const isValid = title.trim() && content.trim();
508
556
  function handleOpenCreate() {
509
557
  setTitle("");
510
- const formContent = extractContentFromForm();
511
- if (formContent) {
512
- setContent(formContent);
558
+ const fields = detectTextFields();
559
+ setAvailableFields(fields);
560
+ if (fields.length > 0) {
561
+ setFieldName(fields[0].name);
562
+ setContent(fields[0].value);
513
563
  }
514
564
  setIsVisible(true);
515
565
  }
@@ -595,16 +645,6 @@ function EmbeddingsModal() {
595
645
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Modal.Root, { open: isVisible, onOpenChange: setIsVisible, children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Modal.Content, { children: [
596
646
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Modal.Header, { children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Modal.Title, { children: "Create Embedding from Content" }) }),
597
647
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Modal.Body, { children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Box, { children: [
598
- /* @__PURE__ */ jsxRuntime.jsxs(StyledTypography, { variant: "omega", textColor: "neutral600", children: [
599
- "Content: ",
600
- contentLength,
601
- " characters",
602
- willChunk && /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Typography, { textColor: "primary600", children: [
603
- " (will create ~",
604
- estimatedChunks,
605
- " embeddings)"
606
- ] })
607
- ] }),
608
648
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { marginBottom: 4, children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Field.Root, { children: [
609
649
  /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: "Title" }),
610
650
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -616,10 +656,38 @@ function EmbeddingsModal() {
616
656
  }
617
657
  )
618
658
  ] }) }),
619
- /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Box, { marginBottom: 4, children: [
659
+ availableFields.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Box, { marginBottom: 4, children: [
620
660
  /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Field.Root, { children: [
621
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: "Content" }),
622
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Hint, { children: fieldName ? `From field: ${fieldName}` : "Enter content manually" })
661
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: "Source Field" }),
662
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Hint, { children: "Select which field to use for the embedding content" })
663
+ ] }),
664
+ /* @__PURE__ */ jsxRuntime.jsx(
665
+ designSystem.SingleSelect,
666
+ {
667
+ value: fieldName,
668
+ onChange: (value) => handleFieldChange(value),
669
+ placeholder: "Select a field",
670
+ children: availableFields.map((field) => /* @__PURE__ */ jsxRuntime.jsxs(designSystem.SingleSelectOption, { value: field.name, children: [
671
+ field.label,
672
+ " (",
673
+ field.charCount.toLocaleString(),
674
+ " chars)"
675
+ ] }, field.name))
676
+ }
677
+ )
678
+ ] }),
679
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Box, { marginBottom: 4, children: [
680
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { justifyContent: "space-between", alignItems: "center", children: [
681
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Root, { children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: "Content Preview" }) }),
682
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Typography, { variant: "pi", textColor: "neutral600", children: [
683
+ contentLength.toLocaleString(),
684
+ " characters",
685
+ willChunk && /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Typography, { textColor: "primary600", children: [
686
+ " (~",
687
+ estimatedChunks,
688
+ " chunks)"
689
+ ] })
690
+ ] })
623
691
  ] }),
624
692
  /* @__PURE__ */ jsxRuntime.jsx(
625
693
  MarkdownEditor,
@@ -1,6 +1,6 @@
1
1
  import { jsxs, Fragment, jsx } 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";
@@ -407,7 +407,7 @@ function MarkdownEditor({ content, onChange, height = 300 }) {
407
407
  )
408
408
  ] });
409
409
  }
410
- const StyledTypography = styled(Typography)`
410
+ styled(Typography)`
411
411
  display: block;
412
412
  margin-top: 1rem;
413
413
  margin-bottom: 0.5rem;
@@ -424,6 +424,7 @@ function EmbeddingsModal() {
424
424
  const [title, setTitle] = useState("");
425
425
  const [content, setContent] = useState("");
426
426
  const [fieldName, setFieldName] = useState("");
427
+ const [availableFields, setAvailableFields] = useState([]);
427
428
  const [isLoading, setIsLoading] = useState(false);
428
429
  const [isCheckingExisting, setIsCheckingExisting] = useState(true);
429
430
  const [existingEmbedding, setExistingEmbedding] = useState(null);
@@ -456,37 +457,84 @@ function EmbeddingsModal() {
456
457
  }
457
458
  checkExistingEmbedding();
458
459
  }, [id, slug, get]);
459
- const extractContentFromForm = useCallback(() => {
460
- if (!modifiedValues) return "";
461
- const textFieldNames = ["content", "description", "body", "text", "richtext", "markdown"];
462
- for (const name of textFieldNames) {
463
- const value = modifiedValues[name];
464
- if (value) {
465
- if (typeof value === "string" && value.trim()) {
466
- setFieldName(name);
467
- return value;
468
- } else if (Array.isArray(value)) {
469
- const text = value.map((block) => {
470
- if (block.children) {
471
- return block.children.map((child) => child.text || "").join("");
472
- }
473
- return "";
474
- }).join("\n\n");
475
- if (text.trim()) {
476
- setFieldName(name);
477
- return text;
460
+ const extractTextFromField = useCallback((value, depth = 0) => {
461
+ if (!value || depth > 5) return "";
462
+ if (typeof value === "string") {
463
+ return value.trim();
464
+ }
465
+ if (Array.isArray(value)) {
466
+ const texts = [];
467
+ for (const item of value) {
468
+ if (item && typeof item === "object" && item.__component) {
469
+ for (const [key, fieldValue] of Object.entries(item)) {
470
+ if (key === "__component" || key === "id") continue;
471
+ const extracted = extractTextFromField(fieldValue, depth + 1);
472
+ if (extracted) texts.push(extracted);
478
473
  }
474
+ } else if (item && item.children) {
475
+ const blockText = item.children.map((child) => child.text || "").join("");
476
+ if (blockText) texts.push(blockText);
477
+ } else if (item && typeof item === "object") {
478
+ const extracted = extractTextFromField(item, depth + 1);
479
+ if (extracted) texts.push(extracted);
479
480
  }
480
481
  }
482
+ return texts.join("\n\n").trim();
483
+ }
484
+ if (typeof value === "object") {
485
+ const texts = [];
486
+ for (const [key, fieldValue] of Object.entries(value)) {
487
+ if (["id", "__component", "documentId", "createdAt", "updatedAt"].includes(key)) continue;
488
+ const extracted = extractTextFromField(fieldValue, depth + 1);
489
+ if (extracted) texts.push(extracted);
490
+ }
491
+ return texts.join("\n\n").trim();
481
492
  }
482
493
  return "";
483
- }, [modifiedValues]);
494
+ }, []);
495
+ const isDynamicZone = (value) => {
496
+ return Array.isArray(value) && value.length > 0 && value[0]?.__component;
497
+ };
498
+ const detectTextFields = useCallback(() => {
499
+ if (!modifiedValues) return [];
500
+ const fields = [];
501
+ for (const [name, value] of Object.entries(modifiedValues)) {
502
+ if (["id", "documentId", "createdAt", "updatedAt", "publishedAt", "locale", "localizations"].includes(name)) {
503
+ continue;
504
+ }
505
+ const textValue = extractTextFromField(value);
506
+ if (textValue && textValue.length > 0) {
507
+ let label = name.replace(/([A-Z])/g, " $1").replace(/^./, (str) => str.toUpperCase()).trim();
508
+ if (isDynamicZone(value)) {
509
+ const componentCount = value.length;
510
+ label += ` (${componentCount} component${componentCount > 1 ? "s" : ""})`;
511
+ }
512
+ fields.push({
513
+ name,
514
+ label,
515
+ value: textValue,
516
+ charCount: textValue.length
517
+ });
518
+ }
519
+ }
520
+ fields.sort((a, b) => b.charCount - a.charCount);
521
+ return fields;
522
+ }, [modifiedValues, extractTextFromField]);
484
523
  useEffect(() => {
485
- const formContent = extractContentFromForm();
486
- if (formContent) {
487
- setContent(formContent);
524
+ const fields = detectTextFields();
525
+ setAvailableFields(fields);
526
+ if (fields.length > 0 && !fieldName) {
527
+ setFieldName(fields[0].name);
528
+ setContent(fields[0].value);
488
529
  }
489
- }, [extractContentFromForm]);
530
+ }, [detectTextFields, fieldName]);
531
+ const handleFieldChange = (selectedFieldName) => {
532
+ setFieldName(selectedFieldName);
533
+ const selectedField = availableFields.find((f) => f.name === selectedFieldName);
534
+ if (selectedField) {
535
+ setContent(selectedField.value);
536
+ }
537
+ };
490
538
  const contentLength = content.length;
491
539
  const willChunk = contentLength > CHUNK_SIZE;
492
540
  const estimatedChunks = willChunk ? Math.ceil(contentLength / (CHUNK_SIZE - 200)) : 1;
@@ -503,9 +551,11 @@ function EmbeddingsModal() {
503
551
  const isValid = title.trim() && content.trim();
504
552
  function handleOpenCreate() {
505
553
  setTitle("");
506
- const formContent = extractContentFromForm();
507
- if (formContent) {
508
- setContent(formContent);
554
+ const fields = detectTextFields();
555
+ setAvailableFields(fields);
556
+ if (fields.length > 0) {
557
+ setFieldName(fields[0].name);
558
+ setContent(fields[0].value);
509
559
  }
510
560
  setIsVisible(true);
511
561
  }
@@ -591,16 +641,6 @@ function EmbeddingsModal() {
591
641
  /* @__PURE__ */ jsx(Modal.Root, { open: isVisible, onOpenChange: setIsVisible, children: /* @__PURE__ */ jsxs(Modal.Content, { children: [
592
642
  /* @__PURE__ */ jsx(Modal.Header, { children: /* @__PURE__ */ jsx(Modal.Title, { children: "Create Embedding from Content" }) }),
593
643
  /* @__PURE__ */ jsx(Modal.Body, { children: /* @__PURE__ */ jsxs(Box, { children: [
594
- /* @__PURE__ */ jsxs(StyledTypography, { variant: "omega", textColor: "neutral600", children: [
595
- "Content: ",
596
- contentLength,
597
- " characters",
598
- willChunk && /* @__PURE__ */ jsxs(Typography, { textColor: "primary600", children: [
599
- " (will create ~",
600
- estimatedChunks,
601
- " embeddings)"
602
- ] })
603
- ] }),
604
644
  /* @__PURE__ */ jsx(Box, { marginBottom: 4, children: /* @__PURE__ */ jsxs(Field.Root, { children: [
605
645
  /* @__PURE__ */ jsx(Field.Label, { children: "Title" }),
606
646
  /* @__PURE__ */ jsx(
@@ -612,10 +652,38 @@ function EmbeddingsModal() {
612
652
  }
613
653
  )
614
654
  ] }) }),
615
- /* @__PURE__ */ jsxs(Box, { marginBottom: 4, children: [
655
+ availableFields.length > 0 && /* @__PURE__ */ jsxs(Box, { marginBottom: 4, children: [
616
656
  /* @__PURE__ */ jsxs(Field.Root, { children: [
617
- /* @__PURE__ */ jsx(Field.Label, { children: "Content" }),
618
- /* @__PURE__ */ jsx(Field.Hint, { children: fieldName ? `From field: ${fieldName}` : "Enter content manually" })
657
+ /* @__PURE__ */ jsx(Field.Label, { children: "Source Field" }),
658
+ /* @__PURE__ */ jsx(Field.Hint, { children: "Select which field to use for the embedding content" })
659
+ ] }),
660
+ /* @__PURE__ */ jsx(
661
+ SingleSelect,
662
+ {
663
+ value: fieldName,
664
+ onChange: (value) => handleFieldChange(value),
665
+ placeholder: "Select a field",
666
+ children: availableFields.map((field) => /* @__PURE__ */ jsxs(SingleSelectOption, { value: field.name, children: [
667
+ field.label,
668
+ " (",
669
+ field.charCount.toLocaleString(),
670
+ " chars)"
671
+ ] }, field.name))
672
+ }
673
+ )
674
+ ] }),
675
+ /* @__PURE__ */ jsxs(Box, { marginBottom: 4, children: [
676
+ /* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "center", children: [
677
+ /* @__PURE__ */ jsx(Field.Root, { children: /* @__PURE__ */ jsx(Field.Label, { children: "Content Preview" }) }),
678
+ /* @__PURE__ */ jsxs(Typography, { variant: "pi", textColor: "neutral600", children: [
679
+ contentLength.toLocaleString(),
680
+ " characters",
681
+ willChunk && /* @__PURE__ */ jsxs(Typography, { textColor: "primary600", children: [
682
+ " (~",
683
+ estimatedChunks,
684
+ " chunks)"
685
+ ] })
686
+ ] })
619
687
  ] }),
620
688
  /* @__PURE__ */ jsx(
621
689
  MarkdownEditor,
@@ -2061,12 +2061,12 @@ const embeddings = ({ strapi }) => ({
2061
2061
  return chunks.length;
2062
2062
  },
2063
2063
  /**
2064
- * Update embeddings with automatic chunking support
2065
- * Handles re-chunking when content changes and exceeds chunk size
2064
+ * Update a single chunk's content and embedding
2065
+ * Updates only the specified chunk without affecting other chunks in the group
2066
2066
  */
2067
2067
  async updateChunkedEmbedding(id, data) {
2068
- const { title, content, metadata, autoChunk } = data.data;
2069
- const config2 = this.getConfig();
2068
+ const { title, content, metadata } = data.data;
2069
+ this.getConfig();
2070
2070
  const currentEntry = await strapi.documents(CONTENT_TYPE_UID$1).findOne({
2071
2071
  documentId: id
2072
2072
  });
@@ -2074,64 +2074,61 @@ const embeddings = ({ strapi }) => ({
2074
2074
  throw new Error(`Embedding with id ${id} not found`);
2075
2075
  }
2076
2076
  const currentMetadata = currentEntry.metadata;
2077
- const parentDocumentId = currentMetadata?.parentDocumentId || id;
2078
- const newContent = content ?? currentEntry.content;
2079
- const newTitle = title ?? currentMetadata?.originalTitle ?? currentEntry.title;
2080
- const shouldChunk = autoChunk ?? config2.autoChunk;
2081
- const chunkSize = config2.chunkSize || 4e3;
2082
- const contentNeedsChunking = shouldChunk && needsChunking(newContent, chunkSize);
2083
- const existingChunks = await this.findRelatedChunks(id);
2084
- let originalRelated;
2085
- const firstChunk = existingChunks.find(
2086
- (c) => c.metadata?.chunkIndex === 0 || c.documentId === parentDocumentId
2087
- );
2088
- if (firstChunk?.related) {
2089
- originalRelated = firstChunk.related;
2090
- }
2091
- const deletedCount = await this.deleteRelatedChunks(id);
2092
- console.log(`Deleted ${deletedCount} existing chunk(s) for update`);
2093
- const preservedMetadata = { ...metadata };
2094
- delete preservedMetadata?.isChunk;
2095
- delete preservedMetadata?.chunkIndex;
2096
- delete preservedMetadata?.totalChunks;
2097
- delete preservedMetadata?.startOffset;
2098
- delete preservedMetadata?.endOffset;
2099
- delete preservedMetadata?.originalTitle;
2100
- delete preservedMetadata?.parentDocumentId;
2101
- delete preservedMetadata?.estimatedTokens;
2102
- if (contentNeedsChunking) {
2103
- return await this.createChunkedEmbedding({
2104
- data: {
2105
- title: newTitle.replace(/\s*\[Part \d+\/\d+\]$/, ""),
2106
- // Remove old part suffix
2107
- content: newContent,
2108
- collectionType: currentEntry.collectionType || "standalone",
2109
- fieldName: currentEntry.fieldName || "content",
2110
- metadata: preservedMetadata,
2111
- related: originalRelated,
2112
- autoChunk: true
2113
- }
2114
- });
2115
- } else {
2116
- const entity = await this.createEmbedding({
2117
- data: {
2118
- title: newTitle.replace(/\s*\[Part \d+\/\d+\]$/, ""),
2119
- // Remove old part suffix
2077
+ const contentChanged = content !== void 0 && content !== currentEntry.content;
2078
+ console.log(`[updateChunkedEmbedding] Updating single chunk ${id}, contentChanged: ${contentChanged}`);
2079
+ const updateData = {};
2080
+ if (title !== void 0) {
2081
+ const currentTitle = currentEntry.title || "";
2082
+ const partMatch = currentTitle.match(/\s*\[Part \d+\/\d+\]$/);
2083
+ updateData.title = partMatch ? `${title}${partMatch[0]}` : title;
2084
+ }
2085
+ if (content !== void 0) {
2086
+ updateData.content = content;
2087
+ }
2088
+ if (metadata !== void 0) {
2089
+ updateData.metadata = {
2090
+ ...currentMetadata,
2091
+ ...metadata
2092
+ };
2093
+ if (contentChanged) {
2094
+ updateData.metadata.estimatedTokens = estimateTokens(updateData.content || currentEntry.content);
2095
+ }
2096
+ }
2097
+ const updatedEntity = await strapi.documents(CONTENT_TYPE_UID$1).update({
2098
+ documentId: id,
2099
+ data: updateData
2100
+ });
2101
+ if (pluginManager.isInitialized() && (contentChanged || title !== void 0)) {
2102
+ try {
2103
+ console.log(`[updateChunkedEmbedding] Updating embedding in Neon for chunk ${id}`);
2104
+ await pluginManager.deleteEmbedding(id);
2105
+ const newContent = updateData.content || currentEntry.content;
2106
+ const result = await pluginManager.createEmbedding({
2107
+ id,
2108
+ title: updatedEntity.title || currentEntry.title,
2120
2109
  content: newContent,
2121
2110
  collectionType: currentEntry.collectionType || "standalone",
2122
- fieldName: currentEntry.fieldName || "content",
2123
- metadata: preservedMetadata,
2124
- related: originalRelated,
2125
- autoChunk: false
2126
- }
2127
- });
2128
- return {
2129
- entity,
2130
- chunks: [entity],
2131
- totalChunks: 1,
2132
- wasChunked: false
2133
- };
2111
+ fieldName: currentEntry.fieldName || "content"
2112
+ });
2113
+ await strapi.documents(CONTENT_TYPE_UID$1).update({
2114
+ documentId: id,
2115
+ data: {
2116
+ embeddingId: result.embeddingId,
2117
+ embedding: result.embedding
2118
+ }
2119
+ });
2120
+ console.log(`[updateChunkedEmbedding] Successfully updated embedding for chunk ${id}`);
2121
+ } catch (error) {
2122
+ console.error(`Failed to update vector store for ${id}:`, error);
2123
+ }
2134
2124
  }
2125
+ const allChunks = await this.findRelatedChunks(id);
2126
+ return {
2127
+ entity: updatedEntity,
2128
+ chunks: allChunks,
2129
+ totalChunks: allChunks.length,
2130
+ wasChunked: allChunks.length > 1
2131
+ };
2135
2132
  },
2136
2133
  async updateEmbedding(id, data) {
2137
2134
  const { title, content: rawContent, metadata, autoChunk } = data.data;
@@ -2058,12 +2058,12 @@ const embeddings = ({ strapi }) => ({
2058
2058
  return chunks.length;
2059
2059
  },
2060
2060
  /**
2061
- * Update embeddings with automatic chunking support
2062
- * Handles re-chunking when content changes and exceeds chunk size
2061
+ * Update a single chunk's content and embedding
2062
+ * Updates only the specified chunk without affecting other chunks in the group
2063
2063
  */
2064
2064
  async updateChunkedEmbedding(id, data) {
2065
- const { title, content, metadata, autoChunk } = data.data;
2066
- const config2 = this.getConfig();
2065
+ const { title, content, metadata } = data.data;
2066
+ this.getConfig();
2067
2067
  const currentEntry = await strapi.documents(CONTENT_TYPE_UID$1).findOne({
2068
2068
  documentId: id
2069
2069
  });
@@ -2071,64 +2071,61 @@ const embeddings = ({ strapi }) => ({
2071
2071
  throw new Error(`Embedding with id ${id} not found`);
2072
2072
  }
2073
2073
  const currentMetadata = currentEntry.metadata;
2074
- const parentDocumentId = currentMetadata?.parentDocumentId || id;
2075
- const newContent = content ?? currentEntry.content;
2076
- const newTitle = title ?? currentMetadata?.originalTitle ?? currentEntry.title;
2077
- const shouldChunk = autoChunk ?? config2.autoChunk;
2078
- const chunkSize = config2.chunkSize || 4e3;
2079
- const contentNeedsChunking = shouldChunk && needsChunking(newContent, chunkSize);
2080
- const existingChunks = await this.findRelatedChunks(id);
2081
- let originalRelated;
2082
- const firstChunk = existingChunks.find(
2083
- (c) => c.metadata?.chunkIndex === 0 || c.documentId === parentDocumentId
2084
- );
2085
- if (firstChunk?.related) {
2086
- originalRelated = firstChunk.related;
2087
- }
2088
- const deletedCount = await this.deleteRelatedChunks(id);
2089
- console.log(`Deleted ${deletedCount} existing chunk(s) for update`);
2090
- const preservedMetadata = { ...metadata };
2091
- delete preservedMetadata?.isChunk;
2092
- delete preservedMetadata?.chunkIndex;
2093
- delete preservedMetadata?.totalChunks;
2094
- delete preservedMetadata?.startOffset;
2095
- delete preservedMetadata?.endOffset;
2096
- delete preservedMetadata?.originalTitle;
2097
- delete preservedMetadata?.parentDocumentId;
2098
- delete preservedMetadata?.estimatedTokens;
2099
- if (contentNeedsChunking) {
2100
- return await this.createChunkedEmbedding({
2101
- data: {
2102
- title: newTitle.replace(/\s*\[Part \d+\/\d+\]$/, ""),
2103
- // Remove old part suffix
2104
- content: newContent,
2105
- collectionType: currentEntry.collectionType || "standalone",
2106
- fieldName: currentEntry.fieldName || "content",
2107
- metadata: preservedMetadata,
2108
- related: originalRelated,
2109
- autoChunk: true
2110
- }
2111
- });
2112
- } else {
2113
- const entity = await this.createEmbedding({
2114
- data: {
2115
- title: newTitle.replace(/\s*\[Part \d+\/\d+\]$/, ""),
2116
- // Remove old part suffix
2074
+ const contentChanged = content !== void 0 && content !== currentEntry.content;
2075
+ console.log(`[updateChunkedEmbedding] Updating single chunk ${id}, contentChanged: ${contentChanged}`);
2076
+ const updateData = {};
2077
+ if (title !== void 0) {
2078
+ const currentTitle = currentEntry.title || "";
2079
+ const partMatch = currentTitle.match(/\s*\[Part \d+\/\d+\]$/);
2080
+ updateData.title = partMatch ? `${title}${partMatch[0]}` : title;
2081
+ }
2082
+ if (content !== void 0) {
2083
+ updateData.content = content;
2084
+ }
2085
+ if (metadata !== void 0) {
2086
+ updateData.metadata = {
2087
+ ...currentMetadata,
2088
+ ...metadata
2089
+ };
2090
+ if (contentChanged) {
2091
+ updateData.metadata.estimatedTokens = estimateTokens(updateData.content || currentEntry.content);
2092
+ }
2093
+ }
2094
+ const updatedEntity = await strapi.documents(CONTENT_TYPE_UID$1).update({
2095
+ documentId: id,
2096
+ data: updateData
2097
+ });
2098
+ if (pluginManager.isInitialized() && (contentChanged || title !== void 0)) {
2099
+ try {
2100
+ console.log(`[updateChunkedEmbedding] Updating embedding in Neon for chunk ${id}`);
2101
+ await pluginManager.deleteEmbedding(id);
2102
+ const newContent = updateData.content || currentEntry.content;
2103
+ const result = await pluginManager.createEmbedding({
2104
+ id,
2105
+ title: updatedEntity.title || currentEntry.title,
2117
2106
  content: newContent,
2118
2107
  collectionType: currentEntry.collectionType || "standalone",
2119
- fieldName: currentEntry.fieldName || "content",
2120
- metadata: preservedMetadata,
2121
- related: originalRelated,
2122
- autoChunk: false
2123
- }
2124
- });
2125
- return {
2126
- entity,
2127
- chunks: [entity],
2128
- totalChunks: 1,
2129
- wasChunked: false
2130
- };
2108
+ fieldName: currentEntry.fieldName || "content"
2109
+ });
2110
+ await strapi.documents(CONTENT_TYPE_UID$1).update({
2111
+ documentId: id,
2112
+ data: {
2113
+ embeddingId: result.embeddingId,
2114
+ embedding: result.embedding
2115
+ }
2116
+ });
2117
+ console.log(`[updateChunkedEmbedding] Successfully updated embedding for chunk ${id}`);
2118
+ } catch (error) {
2119
+ console.error(`Failed to update vector store for ${id}:`, error);
2120
+ }
2131
2121
  }
2122
+ const allChunks = await this.findRelatedChunks(id);
2123
+ return {
2124
+ entity: updatedEntity,
2125
+ chunks: allChunks,
2126
+ totalChunks: allChunks.length,
2127
+ wasChunked: allChunks.length > 1
2128
+ };
2132
2129
  },
2133
2130
  async updateEmbedding(id, data) {
2134
2131
  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.2.1",
3
+ "version": "0.2.2",
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",
@@ -26,7 +26,7 @@
26
26
  },
27
27
  "type": "commonjs",
28
28
  "overrides": {
29
- "@langchain/core": "1.1.9"
29
+ "@langchain/core": "^1.1.31"
30
30
  },
31
31
  "exports": {
32
32
  "./package.json": "./package.json",
@@ -59,7 +59,7 @@
59
59
  },
60
60
  "dependencies": {
61
61
  "@langchain/community": "^1.1.2",
62
- "@langchain/core": "1.1.9",
62
+ "@langchain/core": "^1.1.31",
63
63
  "@langchain/openai": "^1.2.1",
64
64
  "@mdxeditor/editor": "^3.52.3",
65
65
  "@modelcontextprotocol/sdk": "^1.12.0",