import { InvestigationStatus } from "@typez/investigation/enums";
import { getLogger } from "@utils/asyncLocalStorage";
import { hasArrayValue, hasNumberValue, hasStringValue } from "@utils/hasValue";
/**
* InvestigationBuilder
* @category Services
*/
export class InvestigationBuilder {
/**
* Field mappings for string editable fields
* @category Services
*/
static STRING_FIELD_MAPPINGS = [
{ dtoField: "title", investigationField: "title" },
{ dtoField: "curriculum", investigationField: "curriculum" },
{ dtoField: "unitNumberAndTitle", investigationField: "unitNumberAndTitle" },
{ dtoField: "grade", investigationField: "grade" },
{ dtoField: "lessonNumberAndTitle", investigationField: "lessonNumberAndTitle" },
{ dtoField: "objectives", investigationField: "objectives" },
{ dtoField: "ngss", investigationField: "ngss" },
{ dtoField: "analyticalFacts", investigationField: "analyticalFacts" },
{ dtoField: "goals", investigationField: "goals" },
{ dtoField: "day", investigationField: "day" },
];
/**
* Field mappings for number editable fields
*/
static NUMBER_FIELD_MAPPINGS = [{ dtoField: "day", investigationField: "day" }];
/**
* Field mappings for array editable fields
*/
static ARRAY_FIELD_MAPPINGS = [{ dtoField: "objects", investigationField: "objects" }];
/**
* Builds investigation data for creation with global options.
* @category Services
* @param {ICreateInvestigationDto} createDto - The validated request data.
* @param {boolean} aiAssistantFlow - Whether this is an AI-assisted creation (default: false).
* @param {InvestigationStatus} investigationStatus - The investigation status (default: DRAFT_INCOMPLETE).
* @returns {Partial<IInvestigation>} A clean investigation object ready for database storage.
*/
static build(createDto, aiAssistantFlow = false, investigationStatus = InvestigationStatus.DRAFT_INCOMPLETE) {
const builder = {
// Lifecycle & locking fields (always included)
status: investigationStatus,
isLocked: false,
lockedBy: null,
lockedAt: null,
};
// Build editable fields only if they have values
this.buildStringFields(builder, createDto, aiAssistantFlow);
// this.buildNumberFields(builder, createDto, aiAssistantFlow);
this.buildStepsField(builder, createDto, aiAssistantFlow);
this.buildArrayFields(builder, createDto, aiAssistantFlow);
this.buildMetadata(builder, createDto, aiAssistantFlow);
builder.lastChangeWithAI = createDto.lastChangeWithAI ?? true;
return builder;
}
/**
* Creates MongoDB update fields for setting contradiction properties.
* @category Services
* @param {string} fieldPath - The MongoDB dot notation path (e.g., "steps.0.title", "objects.1.name").
* @param {IDetectedContradiction} contradiction - The detected contradiction object.
* @returns {Record<string, unknown>} An object containing MongoDB update fields for setting the contradiction.
*/
static createContradictionSetFields(fieldPath, contradiction) {
return {
[`${fieldPath}.isContradicting`]: true,
[`${fieldPath}.targetFieldName`]: contradiction.targetFieldName,
[`${fieldPath}.contradictionReason`]: contradiction.contradictionReasoning,
[`${fieldPath}.updatedAt`]: new Date(),
};
}
/**
* Creates MongoDB update fields for resetting contradiction properties.
* @category Services
* @param {string} fieldPath - The MongoDB dot notation path (e.g., "steps.0.title", "objects.1.name").
* @returns {Record<string, unknown>} An object containing MongoDB update fields for resetting contradictions.
*/
static createContradictionResetFields(fieldPath) {
return {
[`${fieldPath}.isContradicting`]: false,
[`${fieldPath}.targetFieldName`]: null,
[`${fieldPath}.contradictionReason`]: null,
[`${fieldPath}.updatedAt`]: new Date(),
};
}
/**
* Checks for contradictions in array fields of an investigation.
* @category Services
* @param {IInvestigationModel} investigation - The investigation to check for contradictions.
* @param {IDetectedContradiction[] | null | undefined} detectedContradiction - The detected contradictions.
* @returns {Record<string, unknown>} An object containing MongoDB update fields for setting or resetting contradictions.
*/
static checkArrayFieldContradiction(investigation, detectedContradiction) {
const updateFields = {};
// Check steps for contradictions
if (investigation.steps && investigation.steps.length > 0) {
investigation.steps.forEach((step, stepIndex) => {
const stepFields = ["title", "descriptionEn", "desiredOutcome", "alternativeOutcome"];
stepFields.forEach((fieldName) => {
const field = step[fieldName];
const contradictionKey = `steps[${stepIndex}].${fieldName}`;
// Check if this field has a contradiction in the new detection
const newContradiction = detectedContradiction?.find((contradiction) => contradiction.fieldName === contradictionKey);
if (newContradiction) {
// New contradiction detected - set contradiction properties
const fieldPath = `steps.${stepIndex}.${fieldName}`;
const setFields = this.createContradictionSetFields(fieldPath, newContradiction);
Object.assign(updateFields, setFields);
}
else if (field?.isContradicting) {
// Field was contradicting but no longer is - reset it
const fieldPath = `steps.${stepIndex}.${fieldName}`;
const resetFields = this.createContradictionResetFields(fieldPath);
Object.assign(updateFields, resetFields);
}
});
});
}
// Check objects for contradictions
if (investigation.objects && investigation.objects.length > 0) {
investigation.objects.forEach((object, objectIndex) => {
const objectFields = ["name"];
objectFields.forEach((fieldName) => {
const field = object[fieldName];
const contradictionKey = `objects[${objectIndex}].${fieldName}`;
// Check if this field has a contradiction in the new detection
const newContradiction = detectedContradiction?.find((contradiction) => contradiction.fieldName === contradictionKey);
if (newContradiction) {
// New contradiction detected - set contradiction properties
const fieldPath = `objects.${objectIndex}.${fieldName}`;
const setFields = this.createContradictionSetFields(fieldPath, newContradiction);
Object.assign(updateFields, setFields);
}
else if (field?.isContradicting) {
// Field was contradicting but no longer is - reset it
const fieldPath = `objects.${objectIndex}.${fieldName}`;
const resetFields = this.createContradictionResetFields(fieldPath);
Object.assign(updateFields, resetFields);
}
});
});
}
return updateFields;
}
/**
* Builds investigation update data for contradiction detection.
* @category Services
* @param {IAssistantContradictionDetectionFormat} detectedContradictionResult - An array of detected contradictions to apply to investigation fields.
* @param {IInvestigationModel} investigation - The current investigation to update.
* @returns {Record<string, unknown>} A MongoDB update object with contradiction flags and metadata for affected fields.
*/
static buildUpdateContradiction(detectedContradictionResult, investigation) {
const logger = getLogger();
logger.debug({
investigationId: investigation._id,
hasDetectedContradiction: detectedContradictionResult.isContradictionDetected,
contradictionsCount: detectedContradictionResult.detectedContradiction?.length || 0,
detectedContradictions: detectedContradictionResult.detectedContradiction,
}, "Starting buildUpdateContradiction");
const updateFields = {};
// Update metadata.dateModified to current date
updateFields["metadata.dateModified"] = new Date();
const fieldsKeysForUpdate = [
"title",
"curriculum",
"unitNumberAndTitle",
"grade",
"lessonNumberAndTitle",
"objectives",
"ngss",
"analyticalFacts",
"goals",
"day",
];
if (!detectedContradictionResult.isContradictionDetected) {
for (const fieldName of fieldsKeysForUpdate) {
// For array fields (steps, objects), we don't check isContradicting on the array itself
// For other fields, check if they're not contradicting to skip unnecessary updates
const fieldValue = investigation[fieldName];
const wasContradicting = fieldValue?.isContradicting;
if (wasContradicting) {
// reset if in the past the field was contradicting !
logger.debug({ investigationId: investigation._id, fieldName }, "Field was contradicting, creating reset fields");
const resetFields = this.createContradictionResetFields(fieldName);
Object.assign(updateFields, resetFields);
}
}
const arrayUpdateFields = this.checkArrayFieldContradiction(investigation, detectedContradictionResult.detectedContradiction);
Object.assign(updateFields, arrayUpdateFields);
return updateFields;
}
//Found contradictions!
for (const fieldName of fieldsKeysForUpdate) {
const contradiction = detectedContradictionResult.detectedContradiction?.find((contradiction) => contradiction.fieldName === fieldName);
if (contradiction) {
// Use dot notation for MongoDB updates
updateFields[`${fieldName}.isContradicting`] = true;
updateFields[`${fieldName}.targetFieldName`] = contradiction.targetFieldName;
updateFields[`${fieldName}.contradictionReason`] = contradiction.contradictionReasoning;
updateFields[`${fieldName}.updatedAt`] = new Date();
}
else {
// Only reset if the field was previously contradicting
const fieldValue = investigation[fieldName];
if (fieldValue?.isContradicting) {
logger.debug({ investigationId: investigation._id, fieldName }, "Field was contradicting, creating reset fields");
const resetFields = this.createContradictionResetFields(fieldName);
Object.assign(updateFields, resetFields);
}
}
}
// Handle array fields (steps and objects) for contradictions
const arrayUpdateFields = this.checkArrayFieldContradiction(investigation, detectedContradictionResult.detectedContradiction);
Object.assign(updateFields, arrayUpdateFields);
return updateFields;
}
/**
* Builds investigation update data for the new API format.
* @category Services
* @param {IUpdateInvestigationDto} updateDto - The update data in the new format, including string/number fields and step updates.
* @param {boolean} isAiGenerated - Whether the update is AI-generated, which sets aiGeneratedValue fields.
* @param {IInvestigationModel | null} existingInvestigation - Optional existing investigation to preserve values when fields are null.
* @returns {MongoUpdateFields} A MongoDB update object containing field paths for direct database updates.
*/
static buidlUpdate(updateDto, isAiGenerated = false, existingInvestigation = null) {
const updateFields = {};
// Handle string fields - update the value field of EditableFieldSchema
const primitiveFields = [
// string fields
"title",
"curriculum",
"unitNumberAndTitle",
"grade",
"lessonNumberAndTitle",
"objectives",
"ngss",
"analyticalFacts",
"goals",
// number fields
"day",
];
for (const field of primitiveFields) {
const fieldValue = updateDto[field];
if (fieldValue !== undefined) {
if (typeof fieldValue === "string" || typeof fieldValue === "number") {
updateFields[`${field}.value`] = fieldValue;
if (isAiGenerated) {
updateFields[`${field}.aiGeneratedValue`] = fieldValue;
}
}
else if (typeof fieldValue === "object" && !Array.isArray(fieldValue)) {
Object.entries(fieldValue).forEach(([key, value]) => {
updateFields[`${field}.${key}`] = value;
});
}
updateFields[`${field}.updatedAt`] = new Date();
}
}
// Handle objects array updates
if (updateDto.objects !== undefined) {
// Merge with existing objects to preserve values when fields are null
const existingObjects = existingInvestigation?.objects || [];
updateFields.objects = updateDto.objects.map((obj, index) => this.mapObjectToMongoFormat(obj, isAiGenerated, existingObjects[index] || null));
}
// Handle steps array replacement (supports deletions when fewer steps are sent)
if (updateDto.steps !== undefined) {
// Merge with existing steps to preserve values when fields are null
const existingSteps = existingInvestigation?.steps || [];
updateFields.steps = this.buildEditableStepsFromSimpleSteps(updateDto.steps ?? [], isAiGenerated, existingSteps);
}
// Update metadata.dateModified to current date
updateFields["metadata.dateModified"] = new Date();
return updateFields;
}
/**
* Creates a Vector3 EditableField structure.
* @category Services
* @param {object} vector - The vector data containing x, y, and z coordinates.
* @returns {IEditableField<number> | null} A Vector3 object with an EditableField structure, or `null` if not applicable.
*/
static createVector3EditableField(vector) {
if (!vector)
return null;
return {
x: this.createEditableField(vector.x || 0, false),
y: this.createEditableField(vector.y || 0, false),
z: this.createEditableField(vector.z || 0, false),
};
}
/**
* Maps an API object format to a MongoDB schema format.
* Supports both simple values and {value, aiGeneratedValue} format.
* Preserves existing values when update fields are null/undefined.
* @category Services
* @param {IUpdateObjectDto} obj - The object in API format.
* @param {boolean} isAiGenerated - Whether the update is AI-generated, which sets aiGeneratedValue fields.
* @param {IObjectBase | null} existingObject - Optional existing object to preserve values from.
* @returns {IObjectBase} The object in MongoDB format with an EditableField structure.
*/
static mapObjectToMongoFormat(obj, isAiGenerated = false, existingObject = null) {
// Extract name payload (supports both {value, aiGeneratedValue} and simple string format)
const namePayload = this.extractEditablePayload(obj.name);
const objectIdPayload = this.extractEditablePayload(obj.objectId);
// Preserve existing values if new values are null/undefined
const nameValue = namePayload.value ?? existingObject?.name?.value ?? null;
const nameAiGeneratedValue = namePayload.aiGeneratedValue !== undefined
? namePayload.aiGeneratedValue
: isAiGenerated
? (namePayload.aiGeneratedValue ?? existingObject?.name?.aiGeneratedValue ?? null)
: null;
const objectIdValue = objectIdPayload.value ?? existingObject?.objectId?.value ?? null;
const objectIdAiGeneratedValue = objectIdPayload.aiGeneratedValue !== undefined
? objectIdPayload.aiGeneratedValue
: isAiGenerated
? (namePayload.aiGeneratedValue ?? existingObject?.objectId?.aiGeneratedValue ?? null)
: null;
// Preserve position/rotation if not provided
const position = obj.position !== undefined && obj.position !== null
? this.createVector3EditableField(obj.position)
: existingObject?.position || {
x: this.createEditableField(0, false),
y: this.createEditableField(0, false),
z: this.createEditableField(0, false),
};
const rotation = obj.rotation !== undefined && obj.rotation !== null
? this.createVector3EditableField(obj.rotation)
: existingObject?.rotation || {
x: this.createEditableField(0, false),
y: this.createEditableField(0, false),
z: this.createEditableField(0, false),
};
const sizeValue = obj.size !== undefined ? obj.size : (existingObject?.size?.value ?? null);
return {
name: this.createEditableField(nameValue, isAiGenerated, nameAiGeneratedValue),
objectId: this.createEditableField(objectIdValue, isAiGenerated, objectIdAiGeneratedValue),
position: position || {
x: this.createEditableField(0, false),
y: this.createEditableField(0, false),
z: this.createEditableField(0, false),
},
rotation: rotation || {
x: this.createEditableField(0, false),
y: this.createEditableField(0, false),
z: this.createEditableField(0, false),
},
size: this.createEditableField(sizeValue, false),
};
}
/**
* Builds editable string fields for an investigation.
* @category Services
* @param {Partial<IInvestigation>} builder - Partial investigation object to populate.
* @param {ICreateInvestigationDto} dto - The data transfer object containing investigation creation data.
* @param {boolean} aiAssistantFlow - Whether this is an AI-assisted creation.
*/
static buildStringFields(builder, dto, aiAssistantFlow) {
this.buildEditableFieldsWithMapping(builder, dto, this.STRING_FIELD_MAPPINGS, (value) => hasStringValue(value), aiAssistantFlow);
}
/**
* Builds editable number fields for an investigation.
* @category Services
* @param {Partial<IInvestigation>} builder - Partial investigation object to populate.
* @param {ICreateInvestigationDto} dto - The data transfer object containing investigation creation data.
* @param {boolean} aiAssistantFlow - Whether this is an AI-assisted creation.
*/
static buildNumberFields(builder, dto, aiAssistantFlow) {
this.buildEditableFieldsWithMapping(builder, dto, this.NUMBER_FIELD_MAPPINGS, (value) => hasNumberValue(value), aiAssistantFlow);
}
/**
* Builds the `steps` field for an investigation, with special handling for an array of `IEditableField<IStep>`.
* Converts simple steps from API input into EditableField-wrapped steps for database storage.
* @category Services
* @param {Partial<IInvestigation>} builder - Partial investigation object to populate.
* @param {ICreateInvestigationDto} dto - The data transfer object containing investigation creation data, including steps.
* @param {boolean} aiAssistantFlow - Whether this is an AI-assisted creation.
*/
static buildStepsField(builder, dto, aiAssistantFlow) {
if (hasArrayValue(dto.steps)) {
builder.steps = this.buildEditableStepsFromSimpleSteps(dto.steps, aiAssistantFlow);
}
}
/**
* Builds editable array fields for an investigation.
* @category Services
* @param {Partial<IInvestigation>} builder - Partial investigation object to populate.
* @param {ICreateInvestigationDto} dto - The data transfer object containing investigation creation data, including arrays.
* @param {boolean} aiAssistantFlow - Whether this is an AI-assisted creation.
*/
static buildArrayFields(builder, dto, aiAssistantFlow) {
// Handle objects specially - convert ISimpleObject[] to IObject[]
if (dto.objects && hasArrayValue(dto.objects)) {
builder.objects = this.convertSimpleObjectsToEditableObjects(dto.objects, aiAssistantFlow);
}
// Handle other array fields with generic mapping
const otherArrayMappings = this.ARRAY_FIELD_MAPPINGS.filter((mapping) => mapping.dtoField !== "objects");
if (otherArrayMappings.length > 0) {
this.buildEditableFieldsWithMapping(builder, dto, otherArrayMappings, (value) => hasArrayValue(value), aiAssistantFlow);
}
}
/**
* Converts simple step definitions into editable step models.
* Supports both simple values and {value, aiGeneratedValue} format.
* Preserves existing values when update fields are null/undefined.
* @param {Array} simpleSteps - Steps received from API (can be IStep[] or new format with {value, aiGeneratedValue}).
* @param {boolean} aiAssistantFlow - Whether the data should be marked as AI-generated.
* @param {IInvestigationBaseStepModel[]} existingSteps - Optional existing steps to preserve values from.
* @returns {IInvestigationBaseStepModel[]} Editable steps ready for persistence.
*/
static buildEditableStepsFromSimpleSteps(simpleSteps, aiAssistantFlow, existingSteps = []) {
if (!Array.isArray(simpleSteps)) {
return [];
}
return simpleSteps.map((simpleStep, index) => this.createEditableStep(simpleStep ?? null, aiAssistantFlow, existingSteps[index] || null));
}
/**
* Extracts value and aiGeneratedValue from a payload that can be either a simple value or {value, aiGeneratedValue} object.
* @param {unknown} input - The input value (can be simple value or object with value/aiGeneratedValue).
* @returns {object} Object with value, aiGeneratedValue, and hasAiGeneratedValue flag.
*/
static extractEditablePayload(input) {
if (input && typeof input === "object" && !Array.isArray(input)) {
const candidate = input;
if ("value" in candidate || "aiGeneratedValue" in candidate) {
return {
value: candidate.value,
aiGeneratedValue: candidate.aiGeneratedValue,
hasAiGeneratedValue: Object.prototype.hasOwnProperty.call(candidate, "aiGeneratedValue"),
};
}
}
return {
value: input,
hasAiGeneratedValue: false,
};
}
/**
* Creates an editable step model from a simple step.
* Supports both simple values and {value, aiGeneratedValue} format for backward compatibility.
* Preserves existing values when update fields are null/undefined.
* @param {object | null} simpleStep - Step data from the API payload.
* @param {boolean} aiAssistantFlow - Whether the data should be marked as AI-generated.
* @param {IInvestigationBaseStepModel | null} existingStep - Optional existing step to preserve values from.
* @returns {IInvestigationBaseStepModel} Editable step structure.
*/
static createEditableStep(simpleStep, aiAssistantFlow, existingStep = null) {
const wrappedStep = {};
// Helper to create editable field from payload (supports both formats)
// Preserves existing values when new values are null/undefined
const createField = (fieldValue, fieldName, defaultVal = null) => {
const payload = this.extractEditablePayload(fieldValue);
const existingField = existingStep?.[fieldName];
// Use new value if provided, otherwise preserve existing value, otherwise use default
const value = payload.value !== undefined && payload.value !== null
? payload.value
: (existingField?.value ?? defaultVal);
// Use new aiGeneratedValue if explicitly provided, otherwise preserve existing, otherwise null
const aiGeneratedValue = payload.hasAiGeneratedValue
? payload.aiGeneratedValue
: (payload.value ?? existingField?.aiGeneratedValue ?? null);
return this.createEditableField(value, aiAssistantFlow, aiGeneratedValue);
};
if (simpleStep?.title !== undefined || existingStep?.title) {
wrappedStep.title = createField(simpleStep?.title, "title", "");
}
if (simpleStep?.desiredOutcome !== undefined || existingStep?.desiredOutcome) {
wrappedStep.desiredOutcome = createField(simpleStep?.desiredOutcome, "desiredOutcome", "");
}
if (simpleStep?.alternativeOutcome !== undefined || existingStep?.alternativeOutcome) {
wrappedStep.alternativeOutcome = createField(simpleStep?.alternativeOutcome, "alternativeOutcome", "");
}
if (simpleStep?.descriptionEn !== undefined || existingStep?.descriptionEn) {
wrappedStep.descriptionEn = createField(simpleStep?.descriptionEn, "descriptionEn", "");
}
wrappedStep.skippable = createField(simpleStep?.skippable, "skippable", true);
wrappedStep.skippable_after_marking = createField(simpleStep?.skippable_after_marking, "skippable_after_marking", true);
return wrappedStep;
}
/**
* Converts an array of `ISimpleObject` to an array of `IObjectBase` with an EditableField structure.
* @category Services
* @param {ISimpleObject[]} simpleObjects - The array of simple objects to convert.
* @param {boolean} aiAssistantFlow - Whether this conversion is part of an AI-assisted creation.
* @returns {IObjectBase[]} An array of `IObjectBase` objects with EditableField-wrapped properties.
*/
static convertSimpleObjectsToEditableObjects(simpleObjects, aiAssistantFlow) {
return simpleObjects.map((simpleObject) => ({
name: this.createEditableField(simpleObject.name, aiAssistantFlow),
objectId: this.createEditableField(simpleObject.objectId, aiAssistantFlow),
position: {
x: this.createEditableField(simpleObject.position?.x ?? 0, aiAssistantFlow),
y: this.createEditableField(simpleObject.position?.y ?? 0, aiAssistantFlow),
z: this.createEditableField(simpleObject.position?.z ?? 0, aiAssistantFlow),
},
rotation: {
x: this.createEditableField(simpleObject.rotation?.x ?? 0, aiAssistantFlow),
y: this.createEditableField(simpleObject.rotation?.y ?? 0, aiAssistantFlow),
z: this.createEditableField(simpleObject.rotation?.z ?? 0, aiAssistantFlow),
},
size: this.createEditableField(simpleObject.size ?? 1, aiAssistantFlow),
}));
}
/**
* Type-safe helper to build editable fields using field mappings.
* @category Services
* @param {Partial<IInvestigation>} builder - Partial investigation object to populate.
* @param {ICreateInvestigationDto} dto - The data transfer object containing investigation creation data.
* @param {Array<IFieldMapping>} fieldMappings - An array of mappings between DTO fields and investigation fields.
* @param {Function} hasValueFunction - A function to check if a value is valid for inclusion.
* @param {boolean} aiAssistantFlow - Whether this is an AI-assisted creation.
*/
static buildEditableFieldsWithMapping(builder, dto, fieldMappings, hasValueFunction, aiAssistantFlow) {
for (const mapping of fieldMappings) {
const value = dto[mapping.dtoField];
if (hasValueFunction(value)) {
// Use the mapped investigation field name for type safety
builder[mapping.investigationField] = this.createEditableField(value, aiAssistantFlow);
}
}
}
/**
* Builds metadata and initializes observers for an investigation.
* @category Services
* @param {Partial<IInvestigation>} builder - Partial investigation object to populate.
* @param {ICreateInvestigationDto} dto - The data transfer object containing investigation creation data.
* @param {boolean} _aiAssistantFlow - Indicates if this is part of an AI-assisted creation (not used in this method).
*/
static buildMetadata(builder, dto, _aiAssistantFlow) {
// Observers (always included, even if empty !)
// Metadata (always included!)
builder.metadata = {
dateOfDevelopment: null,
dateOfDevelopmentDelivery: null,
dateOfPublishing: null,
author: dto?.author || null,
editors: [],
views: 0,
sourceInvestigation: null,
dateOfCreation: new Date(),
dateModified: new Date(),
};
}
/**
* Creates a standard EditableField structure based on `aiAssistantFlow`.
* @category Services
* @param {unknown} value - The value to store in the EditableField.
* @param {boolean} aiAssistantFlow - Whether this is part of an AI-assisted creation.
* @param {unknown | null} aiGeneratedValue - Optional AI-generated value to store separately.
* @returns {IEditableField<unknown>} An EditableField object containing the provided value.
*/
static createEditableField(value, aiAssistantFlow, aiGeneratedValue = null) {
if (aiAssistantFlow) {
// AI-assisted flow: humanEdited = false, aiEditable = true, aiGeneratedValue = value (or provided)
// TODO think if it is appropriate to save aiGeneratedValue for the first time while creation !
return {
value,
aiGeneratedValue: aiGeneratedValue !== null ? aiGeneratedValue : value, // Use provided or same as current value for revert functionality
humanEdited: false,
aiEditable: true,
isContradicting: false,
contradictionReason: null,
updatedBy: null,
};
}
else {
// Human-created flow: humanEdited = true, aiEditable = false, aiGeneratedValue = provided value or null
return {
value,
aiGeneratedValue: aiGeneratedValue ?? null,
humanEdited: true,
aiEditable: false,
isContradicting: false,
contradictionReason: null,
updatedBy: null,
};
}
}
}
Source