import { Metadata } from "@models/Metadata.model";
import { ContradictionDetectorService } from "@services/contradictionDetector.service";
import { IntentProcessorService } from "@services/intentProcessor.service";
import { InvestigationBuilder } from "@services/investigation.builder";
import { InvestigationService } from "@services/investigation.service";
import { Intents, ModifyInvestigationIntents } from "@typez/intent/enums";
import { InvestigationStatus } from "@typez/investigation";
import { getLogger } from "@utils/asyncLocalStorage";
import { getLessonData } from "@utils/lessonsFactory";
import _ from "lodash";
import set from "lodash/set";
/**
* Classificator Service
* @category Services
*/
export class ClassificatorService {
investigationService;
intentProcessor;
contradictionDetector;
/**
* Constructor
* @category Services
*/
constructor() {
this.investigationService = new InvestigationService();
this.intentProcessor = new IntentProcessorService();
this.contradictionDetector = new ContradictionDetectorService();
}
/**
* Processes the classificator according to the detected intent.
* @category Services
* @param {IClassificatorInputFormat} messageData - Data containing the user's message, intent, current investigation, and chat history.
* @returns {Promise<IClassificatorResponseFormat>} - The processed investigation or question message.
* @throws {Error} If something goes wrong when requesting the LLM.
*/
async process(messageData) {
try {
// Mapping of intents to processing methods
const processMethodFactory = {
[Intents.GENERATE_INVESTIGATION]: this.generateInvestigation.bind(this),
[Intents.GENERATE_INVESTIGATION_BY_ASSISTANT]: this.generateInvestigationByAssistant.bind(this),
[Intents.MODIFY_INVESTIGATION]: this.modifyInvestigation.bind(this),
[Intents.RESOLVE_CONTRADICTION]: this.resolveContradiction.bind(this),
[Intents.DELETE_INVESTIGATION]: this.deleteInvestigation.bind(this),
};
const processMethod = processMethodFactory[messageData.intent];
if (!processMethod) {
throw new Error(`No process method found for intent: ${messageData.intent}`);
}
const processResult = await processMethod(messageData);
if (!processResult) {
throw new Error(`Unable to process classificator for ${messageData.intent} intent`);
}
return processResult;
}
catch (error) {
throw error;
}
}
/**
* Generates an investigation based on the knowledge base.
* @category Services
* @param {IClassificatorInputFormat} messageData - Data containing the user's message, intent, current investigation, and chat history.
* @returns {Promise<IClassificatorResponseFormat>} - The generated investigation or question message.
* @throws {Error} If something goes wrong when requesting the LLM.
*/
async generateInvestigation(messageData) {
const logger = getLogger();
try {
logger.info("Started generating investigation based on the knowledge base.");
let retry = 0;
let detectedUnitResponse = null;
while (retry < 3) {
detectedUnitResponse = await this.intentProcessor.detectUnitFromMessage(messageData.message, messageData.history);
if (!detectedUnitResponse?.question || !detectedUnitResponse?.unit) {
retry += 1;
}
else if (detectedUnitResponse.question != "-") {
return {
message: detectedUnitResponse.question,
};
}
else if (detectedUnitResponse.unit != "-") {
break;
}
else {
retry += 1;
}
}
if (detectedUnitResponse === null) {
logger.error({
error: "Failed to generate investigation",
retry: retry,
userMessage: messageData.message,
});
throw new Error("Unable to generate investigation: Please try again later!!!");
}
const unit = detectedUnitResponse?.unit;
const lessonDataWithInvestigation = await getLessonData(unit, true);
let investigationMetadata;
retry = 0;
while (retry < 3) {
const detectedLessonWithInvestigationResponse = await this.intentProcessor.detectLessonWithInvestigationFromMessage(messageData.message, lessonDataWithInvestigation, messageData.history);
if (detectedLessonWithInvestigationResponse.question === "-") {
if (detectedLessonWithInvestigationResponse.lesson === "-") {
const lessonDataWithoutInvestigation = await getLessonData(unit, false);
let retryForDetectLessonWithoutInvestigation = 0;
while (retryForDetectLessonWithoutInvestigation < 3) {
const detectedLessonWithoutInvestigationResponse = await this.intentProcessor.detectLessonWithoutInvestigationFromMessage(messageData.message, lessonDataWithoutInvestigation, messageData.history);
if (detectedLessonWithoutInvestigationResponse.question != "-") {
return {
message: detectedLessonWithoutInvestigationResponse.question,
};
}
if (detectedLessonWithoutInvestigationResponse.lesson === "-") {
retryForDetectLessonWithoutInvestigation += 1;
}
else {
investigationMetadata = await Metadata.findOne({
unit: unit,
lessonNumber: parseInt(detectedLessonWithoutInvestigationResponse.lesson, 10),
});
if (investigationMetadata?.hasInvestigation === true) {
retryForDetectLessonWithoutInvestigation += 1;
}
else {
break;
}
}
}
if (investigationMetadata) {
break;
}
else {
retry += 1;
}
}
else {
investigationMetadata = await Metadata.findOne({
unit: unit,
lessonNumber: parseInt(detectedLessonWithInvestigationResponse.lesson, 10),
});
if (investigationMetadata?.hasInvestigation === false) {
retry += 1;
}
else {
break;
}
}
}
else {
return {
message: detectedLessonWithInvestigationResponse.question,
};
}
}
if (retry === 3 || !investigationMetadata) {
logger.error({
error: `Failed to generate investigation: Unable to detect a lesson for unit ${unit}. Switching to assistant knowledge.`,
retry: retry,
userMessage: messageData.message,
});
messageData.attempts = messageData.attempts ? messageData.attempts + 1 : 1;
if (messageData.attempts >= 3) {
throw new Error("Unable to generate investigation: Please try again later!!!");
}
return this.generateInvestigationByAssistant(messageData);
// throw new Error("Unable to generate investigation: Please try again later!!!");
}
const investigation = (await this.investigationService.generateInvestigation(messageData.message, messageData.history, investigationMetadata));
logger.info({ AIGeneratedInvestigation: investigation });
if (!investigation) {
throw new Error("Unable to generate investigation based on the knowledge base");
}
let investigationData = InvestigationBuilder.build(investigation, true, InvestigationStatus.DRAFT_NON_CONTRADICTORY_COMPLETE);
if (messageData.investigationModel) {
investigationData = this.mergeInvestigationPreservingUserEdits(messageData.investigationModel, investigationData);
}
logger.info({ generated: investigationData }, "Investigation has been successfully generated based on the knowledge base.");
return {
message: "Your investigation is ready! Please check it out.",
data: investigationData,
};
}
catch (error) {
throw error;
}
}
/**
* Generates an investigation based on the Assistant's knowledge.
* @param {IClassificatorInputFormat} messageData - Data containing the user's message, intent, current investigation, and chat history.
* @returns {Promise<IClassificatorResponseFormat>} - The generated investigation or question message.
* @throws {Error} If something goes wrong when requesting the LLM.
*/
async generateInvestigationByAssistant(messageData) {
const logger = getLogger();
try {
logger.info("Started generating investigation based on the assistant knowledge.");
let investigationMetadata;
if (messageData.lessonWithoutInvestigation !== null) {
investigationMetadata = await Metadata.findOne({
unit: messageData.lessonWithoutInvestigation?.split("_")[0],
lessonNumber: parseInt(messageData.lessonWithoutInvestigation?.split("_")[1], 10),
});
}
if (!investigationMetadata || messageData.lessonWithoutInvestigation === null) {
let retry = 0;
let detectedUnitResponse = null;
while (retry < 3) {
detectedUnitResponse = await this.intentProcessor.detectUnitFromMessage(messageData.message, messageData.history);
if (detectedUnitResponse.question != "-") {
return {
message: detectedUnitResponse.question,
};
}
if (detectedUnitResponse.unit === "-") {
retry += 1;
}
else {
break;
}
}
if (detectedUnitResponse === null) {
logger.error({
error: "Failed to generate investigation",
retry: retry,
userMessage: messageData.message,
});
throw new Error("Unable to generate investigation: Please try again later!!!");
}
const unit = detectedUnitResponse?.unit;
const lessonDataWithoutInvestigation = await getLessonData(unit, false);
retry = 0;
while (retry < 3) {
const detectedLessonWithoutInvestigationResponse = await this.intentProcessor.detectLessonWithInvestigationFromMessage(messageData.message, lessonDataWithoutInvestigation, messageData.history);
if (detectedLessonWithoutInvestigationResponse.question != "-") {
return {
message: detectedLessonWithoutInvestigationResponse.question,
};
}
if (detectedLessonWithoutInvestigationResponse.lesson === "-") {
retry += 1;
}
else {
investigationMetadata = await Metadata.findOne({
unit: unit,
lessonNumber: parseInt(detectedLessonWithoutInvestigationResponse.lesson, 10),
});
if (investigationMetadata?.hasInvestigation === true) {
retry += 1;
}
else {
break;
}
}
}
if (retry === 3) {
logger.error({
error: `Failed to generate investigation: Unable to detect a lesson for unit ${unit}. switching to not assistant knowledge.`,
retry: retry,
userMessage: messageData.message,
});
messageData.attempts = messageData.attempts ? messageData.attempts + 1 : 1;
if (messageData.attempts >= 3) {
throw new Error("Unable to generate investigation: Please try again later!!!");
}
return this.generateInvestigation(messageData);
}
}
const investigation = (await this.investigationService.generateInvestigation(messageData.message, messageData.history, investigationMetadata));
logger.info({ AIGeneratedInvestigation: investigation });
if (!investigation) {
throw new Error("Unable to generate investigation based on the assistant knowledge");
}
let investigationData = InvestigationBuilder.build(investigation, true, InvestigationStatus.DRAFT_NON_CONTRADICTORY_COMPLETE);
if (messageData.investigationModel) {
investigationData = this.mergeInvestigationPreservingUserEdits(messageData.investigationModel, investigationData);
}
logger.info({ generated: investigationData }, "Investigation has been successfully generated based on the assistant knowledge.");
return {
message: "Your investigation is ready! Please check it out.",
data: investigationData,
};
}
catch (error) {
throw error;
}
}
/**
* Deletes an investigation.
* @category Services
* @param {IClassificatorInputFormat} messageData - Data containing the user's message, intent, current investigation, and chat history.
* @returns {Promise<IClassificatorResponseFormat>} - The deleted investigation or question message.
* @throws {Error} If something goes wrong when requesting the LLM.
*/
async deleteInvestigation(messageData) {
const logger = getLogger();
try {
logger.info("Started deleting investigation data.");
let investigationData = this.investigationService.deleteInvestigationData(messageData.investigationModel);
if (messageData.investigationModel) {
messageData.investigationModel = investigationData;
}
logger.info({ generated: investigationData }, "Investigation data has been successfully deleted.");
return Promise.resolve({
message: "Your investigation data is deleted! Please check it out.",
data: investigationData,
});
}
catch (error) {
throw error;
}
}
/**
* Modifies an investigation.
* @category Services
* @param {IClassificatorInputFormat} messageData - Data containing the user's message, intent, current investigation, and chat history.
* @returns {Promise<IClassificatorResponseFormat>} - The modified investigation or question message.
* @throws {Error} If something goes wrong when requesting the LLM.
*/
async modifyInvestigation(messageData) {
const logger = getLogger();
try {
//TODO: query investigation
//const investigation = await Investigation.findOne({id: messageData.investigationId})
logger.info("Started modifying investigation.");
if (!messageData.investigationModel) {
throw new Error("Investigation must be exists in messageData when modifying it.");
}
let modificationIntentResponse = await this.intentProcessor.detectModifyInvestigationIntent(messageData.message, messageData.history, messageData.investigationDto);
// when assistant gives a question for clarifying process
if ("question" in modificationIntentResponse.response) {
return { message: modificationIntentResponse.response.question };
}
// Mapping of intents to processing methods
const modifyInvestigationProcessMethodFactory = {
[ModifyInvestigationIntents.EDIT_FIELD]: this.intentProcessor.editInvestigationField.bind(this.intentProcessor),
[ModifyInvestigationIntents.ADD_FIELD]: this.intentProcessor.addInvestigationField.bind(this.intentProcessor),
[ModifyInvestigationIntents.REMOVE_FIELD]: this.intentProcessor.removeInvestigationField.bind(this.intentProcessor),
};
// map intents to promises
if ("intents" in modificationIntentResponse.response) {
// create an array of promises returning objects
const promises = modificationIntentResponse.response.intents.map(async (intent) => ({
intent,
result: await modifyInvestigationProcessMethodFactory[intent](messageData.message, messageData.history, messageData.investigationDto),
}));
const questions = [];
// await them all
const modificationResults = await Promise.all(promises);
for (const modificationResult of modificationResults) {
const value = modificationResult.result;
if ("question" in value.response && typeof value.response.question === "string") {
questions.push(value.response.question);
}
}
if (questions.length > 0) {
let summarizedQuestion = questions[0];
if (questions.length > 1) {
summarizedQuestion = await this.intentProcessor.summarizeMessages(questions);
}
return { message: summarizedQuestion };
}
const response = await this.processModificationSteps(messageData, modificationResults);
logger.info("Investigation has been successfully modified.");
return { message: response, data: messageData.investigationModel };
}
throw new Error("Something went wrong when modifying investigation.");
}
catch (error) {
throw error;
}
}
/**
* Resolves a contradiction.
* @category Services
* @param {IClassificatorInputFormat} messageData - Data containing the user's message, intent, current investigation, and chat history.
* @returns {Promise<IClassificatorResponseFormat>} - The investigation with the resolved contradiction.
* @throws {Error} If something goes wrong when requesting the LLM.
*/
async resolveContradiction(messageData) {
const logger = getLogger();
try {
logger.info("Started resolving contradiction.");
const contradictionResolutionMessage = await this.contradictionDetector.resolveContradiction(messageData.investigationModel);
if (!contradictionResolutionMessage) {
throw new Error("Unable to resolve contradiction");
}
logger.info("Contradiction has been successfully resolved.");
return {
message: contradictionResolutionMessage,
data: messageData.investigationModel,
};
}
catch (error) {
throw error;
}
}
/**
* Processes modification steps.
* @category Services
* @param {IClassificatorInputFormat} messageData - Data containing the user's message, intent, current investigation, and chat history.
* @param {IModifyInvestigationProcessFormat[]} modificationSteps - The detected steps that need to be modified in the current investigation.
* @returns {Promise<string>} - A message describing the modifications.
* @throws {Error} If something goes wrong when requesting the LLM.
*/
async processModificationSteps(messageData, modificationSteps) {
try {
// Mapping of intents to processing methods
const modificationStepsFactory = {
[ModifyInvestigationIntents.EDIT_FIELD]: this.editInvestigationFields.bind(this),
[ModifyInvestigationIntents.ADD_FIELD]: this.addInvestigationFields.bind(this),
[ModifyInvestigationIntents.REMOVE_FIELD]: this.removeInvestigationFields.bind(this),
// [ModifyInvestigationIntents.REGENERATE_FIELD]: this.regenerateInvestigationFields.bind(this),
// [ModifyInvestigationIntents.RESOLVE_CONTRADICTION]: this.resolveContradiction.bind(this),
};
const isUserChangeExist = await this.intentProcessor.detectUserChangeExist(messageData.message, messageData.history);
// Store isUserChangeExist on the investigation object for later use in versioning
if (messageData.investigationModel) {
messageData.investigationModel._isUserChangeExist = isUserChangeExist;
}
// create an array of promises returning objects
const promises = modificationSteps.map(async (step) => {
return await modificationStepsFactory[step.intent](messageData.investigationModel, step, isUserChangeExist);
});
// wait for all promises
const results = await Promise.all(promises);
if (results.length > 1) {
return await this.intentProcessor.summarizeMessages(results);
}
return results[0];
}
catch (error) {
throw error;
}
}
/**
* Edits investigation fields.
* @category Services
* @param {IInvestigation} investigation - The current investigation from the database.
* @param {IModifyInvestigationProcessFormat} modificationStep - The detected step for editing fields in the current investigation.
* @param {boolean} isUserChangeExist - If there is user change in the user message
* @returns {Promise<string>} - A message describing the edited fields.
* @throws {Error} If something goes wrong when requesting the LLM.
*/
async editInvestigationFields(investigation, modificationStep, isUserChangeExist) {
try {
const editInvestigationStepResult = modificationStep.result;
const stepResponse = editInvestigationStepResult.response;
for (const key in stepResponse.editableFields) {
if (key == "objects" && stepResponse.editableFields.objects != null) {
for (const [index, object] of stepResponse.editableFields?.objects.entries()) {
for (const objectKey in object) {
set(investigation, `objects[${index}].${objectKey}.value`, object[objectKey]);
if (!isUserChangeExist) {
set(investigation, `objects[${index}].${objectKey}.aiGeneratedValue`, object[objectKey]);
}
}
}
if (stepResponse.editableFields.objects.length < investigation.objects.length) {
const removableObjects = investigation.objects.length - stepResponse.editableFields.objects.length;
investigation.objects.splice(-removableObjects, removableObjects);
}
}
else if (key == "steps" && stepResponse.editableFields.steps != null) {
for (const [index, step] of stepResponse.editableFields?.steps.entries()) {
for (const stepKey in step) {
set(investigation, `steps[${index}].${stepKey}.value`, step[stepKey]);
if (!isUserChangeExist) {
set(investigation, `steps[${index}].${stepKey}.aiGeneratedValue`, step[stepKey]);
}
}
}
if (stepResponse.editableFields.steps.length < investigation.steps.length) {
const removableSteps = investigation.steps.length - stepResponse.editableFields.steps.length;
investigation.steps.splice(-removableSteps, removableSteps);
}
}
else {
set(investigation, `${key}.value`, stepResponse.editableFields[key]);
if (!isUserChangeExist) {
set(investigation, `${key}.aiGeneratedValue`, stepResponse.editableFields[key]);
}
}
}
set(investigation, "metadata.dateModified", new Date());
return await Promise.resolve(stepResponse.changes);
}
catch (error) {
throw error;
}
}
/**
* Adds fields to an investigation.
* @category Services
* @param {IInvestigation} investigation - The current investigation from the database.
* @param {IModifyInvestigationProcessFormat} modificationStep - The detected step for adding data to existing fields in the current investigation.
* @param {boolean} isUserChangeExist - If there is user change in the user message
* @returns {Promise<string>} - A message describing the added data.
* @throws {Error} If something goes wrong when requesting the LLM.
*/
async addInvestigationFields(investigation, modificationStep, isUserChangeExist) {
try {
const addInvestigationStepResult = modificationStep.result;
const stepResponse = addInvestigationStepResult.response;
for (const step of stepResponse.addedFields) {
if (!step.addedField.includes("[")) {
set(investigation, `${step.addedField}.value`, step.content);
if (!isUserChangeExist) {
set(investigation, `${step.addedField}.aiGeneratedValue`, step.content);
}
}
else {
try {
set(investigation, `${step.addedField}`, this.transformObjects(JSON.parse(step.content), isUserChangeExist));
}
catch {
set(investigation, `${step.addedField}`, this.transformObjects(step.content, isUserChangeExist));
}
}
}
set(investigation, "metadata.dateModified", new Date());
return await Promise.resolve(stepResponse.changes);
}
catch (error) {
throw error;
}
}
/**
* Removes fields from an investigation.
* @category Services
* @param {IInvestigation} investigation - The current investigation from the database.
* @param {IModifyInvestigationProcessFormat} modificationStep - The detected step for removing data from the current investigation.
* @returns {Promise<string>} - A message describing the removed data.
* @throws {Error} If something goes wrong when requesting the LLM.
*/
async removeInvestigationFields(investigation, modificationStep) {
try {
const removeInvestigationStepResult = modificationStep.result;
const stepResponse = removeInvestigationStepResult.response;
for (const removedField of stepResponse.removedFields.slice().reverse()) {
const match = removedField.match(/(.*)\[(\d+)\]$/);
if (match) {
const arrayPath = match[1];
const arrayIndex = match[2];
if (!arrayIndex)
continue;
const index = parseInt(arrayIndex, 10);
const arr = _.get(investigation, arrayPath);
if (!Array.isArray(arr))
continue;
// Remove the element by index
const newArr = arr.filter((_, i) => i !== index);
// Replace the array in the document
_.set(investigation, arrayPath, newArr);
}
else {
if (Array.isArray(_.get(investigation, removedField))) {
set(investigation, removedField, []);
}
else if (_.isString(_.get(investigation, `${removedField}.value`))) {
set(investigation, `${removedField}.value`, null);
set(investigation, `${removedField}.aiGeneratedValue`, null);
}
else {
set(investigation, removedField, null);
}
}
}
set(investigation, "metadata.dateModified", new Date());
return await Promise.resolve(stepResponse.changes);
}
catch (error) {
throw error;
}
}
/**
* Transforms a step for adding investigation fields.
* @category Services
* @param {string} obj - The content of the field addition modification step.
* @param {boolean} isUserChangeExist - If there is user change in the user message
* @returns {object} - An object containing `value` and `aiGeneratedValue` fields.
*/
transformObjects(obj, isUserChangeExist) {
return _.mapValues(obj, (value) => {
if (_.isPlainObject(value)) {
return this.transformObjects(value, isUserChangeExist);
}
else {
if (isUserChangeExist) {
return {
value: String(value),
};
}
return {
value: String(value),
aiGeneratedValue: String(value),
};
}
});
}
/**
* Merges AI-generated investigation with existing investigation,
* preserving fields where value !== aiGeneratedValue (user-edited fields).
* @category Services
* @param {IInvestigation} existing - The existing investigation with potentially user-edited fields.
* @param {IInvestigation} generated - The newly AI-generated investigation.
* @returns {IInvestigation} - The merged investigation preserving user-edited fields.
*/
mergeInvestigationPreservingUserEdits(existing, generated) {
const merged = { ...generated };
// List of editable field names
const editableFields = [
"title",
"curriculum",
"unitNumberAndTitle",
"grade",
"lessonNumberAndTitle",
"objectives",
"ngss",
"analyticalFacts",
"goals",
"day",
];
// Check each editable field
for (const fieldName of editableFields) {
const existingField = existing[fieldName];
const generatedField = generated[fieldName];
if (existingField && generatedField) {
// If value !== aiGeneratedValue, it means user edited it - preserve it
const isUserEdited = existingField.value !== null &&
existingField.aiGeneratedValue !== null &&
existingField.value !== existingField.aiGeneratedValue;
if (isUserEdited) {
// Preserve the user-edited field
merged[fieldName] = existingField;
}
// Otherwise, use the generated field (value === aiGeneratedValue means it's still AI-generated)
}
else if (existingField) {
// If field exists in existing but not in generated, preserve it
merged[fieldName] = existingField;
}
}
// Handle steps - preserve user-edited step fields
if (existing.steps && generated.steps) {
merged.steps = generated.steps.map((generatedStep, index) => {
const existingStep = existing.steps?.[index];
if (!existingStep) {
return generatedStep;
}
const mergedStep = { ...generatedStep };
const stepFields = [
"title",
"descriptionEn",
"desiredOutcome",
"alternativeOutcome",
];
for (const stepFieldName of stepFields) {
const existingStepField = existingStep[stepFieldName];
const generatedStepField = generatedStep[stepFieldName];
if (existingStepField && generatedStepField) {
const isUserEdited = existingStepField.value !== null &&
existingStepField.aiGeneratedValue !== null &&
existingStepField.value !== existingStepField.aiGeneratedValue;
if (isUserEdited) {
mergedStep[stepFieldName] = existingStepField;
}
}
else if (existingStepField) {
mergedStep[stepFieldName] = existingStepField;
}
}
return mergedStep;
});
}
else if (existing.steps) {
// If existing has steps but generated doesn't, preserve existing steps
merged.steps = existing.steps;
}
// Handle objects - preserve user-edited object fields
if (existing.objects && generated.objects) {
merged.objects = generated.objects.map((generatedObj, index) => {
const existingObj = existing.objects?.[index];
if (!existingObj) {
return generatedObj;
}
const mergedObj = { ...generatedObj };
// Check name field
if (existingObj.name && generatedObj.name) {
const isUserEdited = existingObj.name.value !== null &&
existingObj.name.aiGeneratedValue !== null &&
existingObj.name.value !== existingObj.name.aiGeneratedValue;
if (isUserEdited) {
mergedObj.name = existingObj.name;
}
}
else if (existingObj.name) {
mergedObj.name = existingObj.name;
}
// Check objectId field
if (existingObj.objectId && generatedObj.objectId) {
const isUserEdited = existingObj.objectId.value !== null &&
existingObj.objectId.aiGeneratedValue !== null &&
existingObj.objectId.value !== existingObj.objectId.aiGeneratedValue;
if (isUserEdited) {
mergedObj.objectId = existingObj.objectId;
}
}
else if (existingObj.objectId) {
mergedObj.objectId = existingObj.objectId;
}
// Check size field
if (existingObj.size && generatedObj.size) {
const isUserEdited = existingObj.size.value !== null &&
existingObj.size.aiGeneratedValue !== null &&
existingObj.size.value !== existingObj.size.aiGeneratedValue;
if (isUserEdited) {
mergedObj.size = existingObj.size;
}
}
else if (existingObj.size) {
mergedObj.size = existingObj.size;
}
return mergedObj;
});
}
else if (existing.objects) {
// If existing has objects but generated doesn't, preserve existing objects
merged.objects = existing.objects;
}
return merged;
}
}
Source