diff --git a/web/libs/editor/src/components/App/App.jsx b/web/libs/editor/src/components/App/App.jsx index c8ddaadcd67f..099d255749b8 100644 --- a/web/libs/editor/src/components/App/App.jsx +++ b/web/libs/editor/src/components/App/App.jsx @@ -15,10 +15,12 @@ import { TreeValidation } from "../TreeValidation/TreeValidation"; /** * Tags */ +// Custom tags should be registered first so all MST models will use their parts like regions and results +import "../../tags/custom"; + import "../../tags/object"; import "../../tags/control"; import "../../tags/visual"; -import "../../tags/Custom"; /** * Utils and common components diff --git a/web/libs/editor/src/regions/Area.js b/web/libs/editor/src/regions/Area.js index d0c385fb822a..034b522738ab 100644 --- a/web/libs/editor/src/regions/Area.js +++ b/web/libs/editor/src/regions/Area.js @@ -17,7 +17,7 @@ import { TimeSeriesRegionModel } from "./TimeSeriesRegion"; import { ParagraphsRegionModel } from "./ParagraphsRegion"; import { VideoRectangleRegionModel } from "./VideoRectangleRegion"; import { BitmaskRegionModel } from "./BitmaskRegion"; -import { CustomRegionModel } from "./CustomRegion"; +import { CustomRegionModel } from "../tags/custom/CustomRegion"; // general Area type for classification Results which doesn't belong to any real Area const ClassificationArea = types.compose( diff --git a/web/libs/editor/src/regions/CustomRegion.js b/web/libs/editor/src/regions/CustomRegion.js deleted file mode 100644 index 89f8ac487abf..000000000000 --- a/web/libs/editor/src/regions/CustomRegion.js +++ /dev/null @@ -1,147 +0,0 @@ -import { observer } from "mobx-react"; -import { types, getParent } from "mobx-state-tree"; - -import NormalizationMixin from "../mixins/Normalization"; -import RegionsMixin from "../mixins/Regions"; -import Registry from "../core/Registry"; -// import { CustomTagModel } from "../tags/Custom"; -import { guidGenerator } from "../core/Helpers"; - -import { HtxTextBox } from "../components/HtxTextBox/HtxTextBox"; -import { cn } from "../utils/bem"; - -const Model = types - .model("CustomRegionModel", { - id: types.optional(types.identifier, guidGenerator), - pid: types.optional(types.string, guidGenerator), - type: "customregion", - - _value: types.frozen(), - // states: types.array(types.union(ChoicesModel)), - }) - .volatile(() => ({ - classification: true, - perRegionTags: [], - results: [], - selected: false, - })) - .views((self) => ({ - get parent() { - // Since getParentOfType might not work, we'll use getParent which should work - // for any MobX parent-child relationship - return getParent(self); - }, - getRegionElement() { - return document.querySelector(`#CustomRegion-${self.id}`); - }, - getOneColor() { - return null; - }, - })) - .actions((self) => ({ - setValue(val) { - if (self._value === val || !self.parent.validateText(val)) return; - - self._value = val; - self.parent.onChange(); - }, - - updateValue(newValue) { - // This is the sanctioned way to update the region's value - // from an external component like the POC UI. - self._value = newValue; - // Also notify the parent about the change so it can update results - if (self.parent.updateResult) { - self.parent.updateResult(); - } - }, - - deleteRegion() { - self.parent.remove(self); - }, - - selectRegion() { - self.selected = true; - }, - - afterUnselectRegion() { - self.selected = false; - }, - })); - -const CustomRegionModel = types.compose("CustomRegionModel", RegionsMixin, NormalizationMixin, Model); - -const HtxCustomRegionView = ({ item, onFocus }) => { - const classes = [styles.mark]; - const params = { onFocus: (e) => onFocus(e, item) }; - const { parent } = item; - const { relationMode } = item.annotation; - const editable = parent.isEditable && !item.isReadOnly(); - const deleteable = parent.isDeleteable && !item.isReadOnly(); - - if (relationMode) { - classes.push(styles.relation); - } - - if (item.selected) { - classes.push(styles.selected); - } else if (item.highlighted) { - classes.push(styles.highlighted); - } - - if (editable || parent.transcription) { - params.onChange = (str) => { - item.setValue(str); - item.parent.updateLeadTime(); - }; - params.onInput = () => { - item.parent.countTime(); - }; - } - - params.onDelete = item.deleteRegion; - - let divAttrs = {}; - - if (!parent.perregion) { - divAttrs = { - onMouseOver: () => { - if (relationMode) { - item.setHighlight(true); - } - }, - onMouseOut: () => { - /* range.setHighlight(false); */ - if (relationMode) { - item.setHighlight(false); - } - }, - }; - } - - const name = `${parent?.name ?? ""}:${item.id}`; - - return ( -
- -
- ); -}; - -const HtxCustomRegion = observer(HtxCustomRegionView); - -Registry.addTag("customregion", CustomRegionModel, HtxCustomRegion); -Registry.addRegionType(CustomRegionModel, "custominterface"); - -export { CustomRegionModel, HtxCustomRegion }; diff --git a/web/libs/editor/src/regions/index.js b/web/libs/editor/src/regions/index.js index 01fb771ef6e2..cc9c16ea4015 100644 --- a/web/libs/editor/src/regions/index.js +++ b/web/libs/editor/src/regions/index.js @@ -15,7 +15,6 @@ import { HtxTextAreaRegion, TextAreaRegionModel } from "./TextAreaRegion"; import { RichTextRegionModel } from "./RichTextRegion"; import { TimelineRegionModel } from "./TimelineRegion"; import { VideoRectangleRegionModel } from "./VideoRectangleRegion"; -import { CustomRegionModel } from "./CustomRegion"; const AllRegionsType = types.union( AudioRegionModel, @@ -33,7 +32,6 @@ const AllRegionsType = types.union( TimelineRegionModel, ParagraphsRegionModel, VideoRectangleRegionModel, - CustomRegionModel, ...Registry.customTags.map((t) => t.region).filter(Boolean), ); @@ -62,5 +60,4 @@ export { TextAreaRegionModel, TimelineRegionModel, VideoRectangleRegionModel, - CustomRegionModel, }; diff --git a/web/libs/editor/src/tags/Custom.jsx b/web/libs/editor/src/tags/Custom.jsx deleted file mode 100644 index b37d99c98c1e..000000000000 --- a/web/libs/editor/src/tags/Custom.jsx +++ /dev/null @@ -1,1079 +0,0 @@ -// Function-based CustomTag for Label Studio -// Code attribute contains a function that receives all Label Studio context -import React from "react"; - -import { destroy, types, getRoot } from "mobx-state-tree"; -import { observer } from "mobx-react"; -import Registry from "../core/Registry"; -import ControlBase from "./control/Base"; -import ClassificationBase from "./control/ClassificationBase"; - -import { AnnotationMixin } from "../mixins/AnnotationMixin"; -import { CustomRegionModel } from "../regions/CustomRegion"; -import { errorBuilder } from "../core/DataValidator/ConfigValidator"; -import { parseValue, tryToParseJSON } from "../utils/data"; -// import * as Babel from '@babel/standalone'; - -// Define the model for the custom tag -const TagAttrs = types.model("CustomIntrefaceAttrs", { - // name: types.identifier, - toname: types.maybeNull(types.string), - - // React component function code as string - code: types.optional(types.string, ""), - - // CDATA/text content (takes priority over code attribute) - value: types.optional(types.string, ""), - - // Data source - can be a $ reference to task data or a literal value (URL, JSON, etc.) - data: types.optional(types.string, ""), - - // Props to pass to the component (JSON string) - props: types.optional(types.string, "{}"), - - // CSS styles for the wrapper - style: types.optional(types.string, ""), - - // CSS classes for the wrapper - classname: types.optional(types.string, ""), - - // Custom CSS to inject - css: types.optional(types.string, ""), - - // Whether to wrap in error boundary - errorBoundary: types.optional(types.boolean, true), -}); - -// Main custom tag model -const Model = types - .model({ - type: "custominterface", - regions: types.array(CustomRegionModel), - globalState: types.optional(types.frozen(), {}), - globalMetadata: types.optional(types.array(types.frozen()), []), - }) - .volatile(() => ({ - loadedData: null, - dataLoaded: false, - dataError: null, - })) - .views((self) => ({ - get store() { - return getRoot(self); - }, - - get annStore() { - return self.annotationStore; - }, - - get result() { - return self.annotation?.results?.find((r) => r.from_name === self); - }, - - get currentValue() { - return self.result?.value || null; - }, - - get parsedProps() { - try { - return JSON.parse(self.props); - } catch (e) { - console.warn("Invalid props JSON:", e); - return {}; - } - }, - - get parsedStyle() { - try { - return self.style ? JSON.parse(self.style) : {}; - } catch (e) { - return {}; - } - }, - - get effectiveCode() { - // Priority: CDATA/text content (value) > code attribute - // Label Studio automatically puts CDATA content into the 'value' property during XML parsing - if (self.value && self.value.trim()) { - return self.value.trim(); - } - if (self.code && self.code.trim()) { - return self.code.trim(); - } - return ""; - }, - - // Required by ClassificationBase mixin - selectedValues() { - // Return the current state to be serialized as the result - const result = { - regions: self.regions.map((r) => r._value), - globalState: self.globalState, - metadata: self.globalMetadata, - }; - console.log("🎯 selectedValues() called, returning:", result); - console.log("Regions array:", self.regions); - console.log("Global state:", self.globalState); - console.log("Metadata:", self.globalMetadata); - return result; - }, - - // Required for classification tags - the type of value this control produces - get valueType() { - return "custom"; - }, - - // Required for result creation - the type of result this control creates - // Must be one of the valid result types that the system recognizes - get resultType() { - return "custominterface"; // Use our custom result type - }, - - // Required for classification tags - indicates if this tag holds state - get holdsState() { - return ( - self.regions.length > 0 || - Object.keys(self.globalState || {}).length > 0 || - (self.globalMetadata && self.globalMetadata.length > 0) - ); - }, - })) - .actions((self) => ({ - setValue(value) { - self.addRegion(value); - }, - - perRegionCleanup() { - // Clear all regions - following the exact pattern from TextArea.jsx - self.regions = []; - // Call updateResult to save the cleared state - self.updateResult(); - }, - - needsUpdate() { - // Called when loading an annotation - restore regions from saved result - // Following the exact pattern from TextArea.jsx - console.log("🔄 needsUpdate called"); - console.log("Current result:", self.result); - console.log("Result mainValue:", self.result?.mainValue); - self.updateFromResult(self.result?.mainValue); - }, - - updateFromResult(value) { - // Restore complete state including regions, global state, and metadata - console.log("📥 updateFromResult called with value:", value); - console.log("Current regions before update:", self.regions.length); - - // Clear current state - self.regions = []; - self.globalState = {}; - self.globalMetadata = []; - - if (value && typeof value === "object") { - // New format: object with regions, state, and metadata - if (value.regions && Array.isArray(value.regions)) { - console.log("Recreating", value.regions.length, "regions from saved data"); - value.regions.forEach((regionValue, index) => { - console.log(`Creating region ${index + 1}:`, regionValue); - self.createRegion(regionValue); - }); - } - - // Restore global state - if (value.globalState && typeof value.globalState === "object") { - console.log("Restoring global state:", value.globalState); - self.globalState = value.globalState; - } - - // Restore metadata - if (value.metadata && Array.isArray(value.metadata)) { - console.log("Restoring metadata:", value.metadata.length, "entries"); - self.globalMetadata = value.metadata; - } - } else if (value && Array.isArray(value)) { - // Legacy format: just array of regions (backward compatibility) - console.log("Recreating", value.length, "regions from legacy format"); - value.forEach((regionValue, index) => { - console.log(`Creating region ${index + 1}:`, regionValue); - self.createRegion(regionValue); - }); - } else { - console.log("No valid saved data to restore"); - } - - console.log("Regions after update:", self.regions.length); - console.log("Global state keys:", Object.keys(self.globalState).length); - console.log("Metadata entries:", self.globalMetadata.length); - }, - - createRegion(value, pid) { - const r = CustomRegionModel.create({ pid, _value: value }); - - self.regions.push(r); - return r; - }, - - addRegion(value) { - // Create a custom region and add it to the regions array - console.log("🚨 UPDATED addRegion method called with value:", value); - console.log("addRegion called with value:", value); - console.log("Current regions before adding:", self.regions.length); - const region = self.createRegion(value); - console.log("Created custom region:", region); - console.log("Current regions after adding:", self.regions.length); - console.log("All regions:", self.regions); - - // Call updateResult to save the new state - following TextArea pattern - console.log("🔄 addRegion called, calling updateResult()"); - try { - self.updateResult(); - console.log("updateResult completed successfully in addRegion"); - } catch (error) { - console.error("Error calling updateResult in addRegion:", error); - } - - return region; - }, - - remove(region) { - // Remove region from the regions array - called by region.deleteRegion() - // Following the exact pattern from TextArea.jsx - const index = self.regions.indexOf(region); - - if (index < 0) return; - self.regions.splice(index, 1); - destroy(region); - - // Call updateResult to save the new state - following TextArea pattern - console.log("🔄 remove region called, calling updateResult()"); - self.updateResult(); - }, - - deleteResult() { - const result = self.annotation.results.find((r) => r.from_name === self); - if (result) { - self.annotation.deleteResult(result); - } - }, - - triggerEvent(eventType, data) { - console.log(`Event ${eventType} triggered on ${self.name}:`, data); - }, - - setLoadedData(data) { - self.loadedData = data; - self.dataLoaded = true; - self.dataError = null; - }, - - setDataError(error) { - self.dataError = error; - self.dataLoaded = false; - self.loadedData = null; - }, - - async preloadData(store) { - if (!self.data) { - // No data attribute specified, skip data loading - self.dataLoaded = true; - return; - } - - const dataObj = store.task.dataObj; - let dataValue; - - // Resolve data value - if starts with $, get from task data, otherwise use literal - if (self.data.startsWith("$")) { - dataValue = parseValue(self.data, dataObj); - } else { - dataValue = self.data; - } - - if (!dataValue) { - self.setDataError(`Cannot resolve data from "${self.data}"`); - return; - } - - // If it's a string that looks like a URL, fetch it - if (typeof dataValue === "string" && /^https?:\/\//.test(dataValue)) { - try { - const response = await fetch(dataValue); - if (!response.ok) { - throw new Error(`${response.status} ${response.statusText}`); - } - const text = await response.text(); - - // Try to parse as JSON first, fall back to text - let parsedData; - try { - parsedData = tryToParseJSON(text) || text; - } catch (e) { - parsedData = text; - } - - self.setLoadedData(parsedData); - } catch (error) { - console.error("Error loading data from URL:", error); - self.setDataError(`Failed to load data from ${dataValue}: ${error.message}`); - store.annotationStore.addErrors([errorBuilder.loadingError(error, dataValue, self.data)]); - } - } else { - // Use data directly (could be JSON object, string, etc.) - self.setLoadedData(dataValue); - } - }, - - // Global state management methods - updateGlobalState(newState) { - // Replace the entire state, don't merge. This ensures keys can be deleted. - self.globalState = newState; - console.log("🔄 updateGlobalState called, SAVING NEW STATE:", newState); - // This is still needed to save the global state which is stored on the - // main result object for the tag. - self.updateResult(); - }, - - // Metadata management methods - addMetadata(action, data) { - const entry = { - timestamp: new Date().toISOString(), - action: action, - data: data, - }; - self.globalMetadata.push(entry); - console.log("🔄 addMetadata called, calling updateResult()"); - self.updateResult(); // Trigger serialization - }, - - removeMetadata(index) { - if (index >= 0 && index < self.globalMetadata.length) { - self.globalMetadata.splice(index, 1); - console.log("🔄 removeMetadata called, calling updateResult()"); - self.updateResult(); // Trigger serialization - } - }, - - clearAllMetadata() { - self.globalMetadata = []; - console.log("🔄 clearAllMetadata called, calling updateResult()"); - self.updateResult(); // Trigger serialization - }, - - // Debug override to see what ClassificationBase does and catch errors - updateResult() { - console.log("🔄 CustomInterface updateResult() called"); - console.log("Current result exists:", !!self.result); - console.log("Selected values:", self.selectedValues()); - console.log("Value type:", self.valueType); - console.log("Annotation:", self.annotation); - console.log("Toname:", self.toname); - - try { - // Try the ClassificationBase approach - if (self.result) { - console.log("Updating existing result"); - console.log("Result area:", self.result.area); - self.result.area.updateOriginOnEdit(); - self.result.area.setValue(self); - console.log("✅ Successfully updated existing result"); - } else { - console.log("Creating new result"); - const resultData = { [self.valueType]: self.selectedValues() }; - console.log("Result data to create:", resultData); - - const newResult = self.annotation.createResult({}, resultData, self, self.toname); - console.log("✅ Successfully created new result:", newResult); - } - - // CRITICAL: Signal that annotation has been modified - // This is what the "Update" button does that we were missing - console.log("🔔 Signaling annotation has been modified"); - - // Mark annotation as having changes (like the Update button does) - self.annotation.history.freeze(); - self.annotation.history.unfreeze(); - - // Mark as user-generated if not already - if (!self.annotation.sentUserGenerate) { - console.log("📝 Marking annotation as user-generated"); - self.annotation.sendUserGenerate(); - } - - // Don't trigger automatic API calls - just mark as dirty - // User can press Update when ready to save to backend - console.log("✅ Annotation marked as modified - ready for manual save"); - - // CRITICAL: Handle automatic saving based on annotation state - const store = getRoot(self); - const annotation = self.annotation; - - console.log("📋 Annotation details:", { - id: annotation.id, - pk: annotation.pk, - exists: annotation.exists, - sentUserGenerate: annotation.sentUserGenerate, - }); - - if (annotation.pk && annotation.exists) { - // Existing annotation - trigger update - console.log("🔄 Triggering automatic update for existing annotation ID:", annotation.pk); - if (store && store.events) { - store.events.invoke("updateAnnotation", store, annotation).catch((error) => { - console.error("❌ Update annotation failed:", error); - }); - } - } else { - // New annotation - trigger submit/create - console.log("➕ Triggering automatic submit for new annotation"); - if (store && store.events) { - store.events.invoke("submitAnnotation", store, annotation).catch((error) => { - console.error("❌ Submit annotation failed:", error); - }); - } - } - - console.log("✅ Annotation update signaling complete"); - } catch (error) { - console.error("❌ Error in updateResult:", error); - console.error("Error details:", { - message: error.message, - stack: error.stack, - result: self.result, - annotation: self.annotation, - valueType: self.valueType, - selectedValues: self.selectedValues(), - }); - } - }, - })); - -const CustomInterfaceModel = types.compose( - "CustomInterfaceModel", - ControlBase, - ClassificationBase, - TagAttrs, - // ...(isFF(FF_LEAD_TIME) ? [LeadTimeMixin] : []), - // ProcessAttrsMixin, - // RequiredMixin, - // PerRegionMixin, - // ...(isFF(FF_LSDV_4583) ? [PerItemMixin] : []), - AnnotationMixin, - // ReadOnlyControlMixin, - Model, -); - -// Error boundary component -class CustomInterfaceErrorBoundary extends React.Component { - constructor(props) { - super(props); - this.state = { hasError: false, error: null, errorInfo: null }; - } - - static getDerivedStateFromError(error) { - return { hasError: true, error }; - } - - componentDidCatch(error, errorInfo) { - console.group("🚨 CustomInterface Error Details"); - console.error("Error:", error); - console.error("Error Info:", errorInfo); - console.error("Component Stack:", errorInfo?.componentStack); - console.error("Function Code:", this.props.code); - console.error("Item Data:", this.props.item); - console.groupEnd(); - - this.setState({ errorInfo }); - } - - render() { - if (this.state.hasError) { - const { error, errorInfo } = this.state; - const { code, item } = this.props; - - return ( -
-

🚨 Custom Component Error

- -
- Error Message: -
-              {error?.toString() || "Unknown error"}
-            
-
- - {error?.stack && ( -
- 📍 Error Stack Trace -
-                {error.stack}
-              
-
- )} - - {errorInfo?.componentStack && ( -
- 🔗 Component Stack -
-                {errorInfo.componentStack}
-              
-
- )} - -
- 📝 Your Function Code -
-              {code || "No code provided"}
-            
-
- -
- 🔍 Context Information -
-

- Tag Name: {item?.name || "Unknown"} -

-

- To Name: {item?.toname || "Unknown"} -

-

- Data Loaded: {item?.dataLoaded ? "Yes" : "No"} -

-

- Data Error: {item?.dataError || "None"} -

-

- Has Code: {code ? "Yes" : "No"} -

-

- Code Length: {code?.length || 0} characters -

-
-
- -
- Debugging Tips: -
    -
  • Check the console for more detailed logs
  • -
  • Verify your function syntax and JSX
  • -
  • Ensure all variables and hooks are properly declared
  • -
  • Check if you're using the correct parameter names
  • -
  • Test your function code in isolation first
  • -
-
-
- ); - } - - return this.props.children; - } -} - -// React component that executes the custom function -const CustomInterfaceComponent = observer(({ item }) => { - const [DynamicComponent, setDynamicComponent] = React.useState(null); - const [error, setError] = React.useState(null); - - // --- ALL HOOKS MUST BE CALLED UNCONDITIONALLY AT THE TOP --- - - // 1. Hook for loading data - React.useEffect(() => { - if (item.annotation?.store && !item.dataLoaded && !item.dataError) { - item.preloadData(item.annotation.store); - } - }, [item.annotation?.store]); - - // 2. Hook for main component logic - React.useEffect(() => { - // Don't run logic until data is loaded. - // This check is *inside* the hook, not outside. - if (!item.dataLoaded) return; - - if (item.dataError) { - setDynamicComponent(() => () => ( -
-

Data Loading Error

-

{item.dataError}

-
- )); - setError(null); - return; - } - - if (!item.effectiveCode.trim()) { - setDynamicComponent(() => () => ( -
-

No function code provided. Add your React component function in the 'code' attribute.

-
-            {``}
-          
-
- )); - setError(null); - return; - } - - try { - const context = { - React, - useState: React.useState, - useEffect: React.useEffect, - useCallback: React.useCallback, - useMemo: React.useMemo, - useRef: React.useRef, - useReducer: React.useReducer, - useContext: React.useContext, - data: item.loadedData, - regions: item.regions, - addRegion: item.addRegion?.bind(item), - deleteRegion: item.remove?.bind(item), - clearAllRegions: item.perRegionCleanup?.bind(item), - state: item.globalState, - saveState: item.updateGlobalState?.bind(item), - saveData: item.updateGlobalState?.bind(item), - metadata: item.globalMetadata, - saveMetadata: item.addMetadata?.bind(item), - deleteMetadata: item.removeMetadata?.bind(item), - clearAllMetadata: item.clearAllMetadata?.bind(item), - tags: () => { - return Array.from(item.annotation.names.values()) - .filter((tag) => tag.type === "choices") - .map((tag) => { - const options = (tag.children || []).map((choice) => choice.value ?? choice._value); - return { - name: tag.name, - type: tag.type, - options, - }; - }); - }, - getTagValue: (tagName) => { - const tag = item.annotation.names.get(tagName); - if (!tag) return null; - const result = item.annotation.results.find((r) => r.from_name === tag); - return result ? result.value : null; - }, - setTagValue: ((tagName, value) => { - const tag = item.annotation.names.get(tagName); - if (!tag) return; - if (tag.type !== "choices") { - console.warn( - `POC LIMITATION: setTagValue only supports 'choices' tags, got '${tag.type}' for tag '${tagName}'`, - ); - alert(`POC Limitation: Only Choices tags are currently supported. Tag '${tagName}' is type '${tag.type}'.`); - return; - } - let formattedValue = value; - if (tag.type === "choices") { - formattedValue = Array.isArray(value) ? value : [value]; - } - const existingResult = item.annotation.results.find((r) => r.from_name === tag); - if (existingResult) { - if (existingResult.setValue) { - existingResult.setValue(formattedValue); - } else { - existingResult.value = formattedValue; - } - } else { - item.annotation.createResult({}, { [tag.valueType]: formattedValue }, tag, tag.toname); - } - if (typeof item.updateResult === "function") { - item.updateResult(); - } - }).bind(item), - item, - annotation: item.annotation, - store: item.annotation?.store || null, - task: item.annotation?.task || null, - getAllResults: () => item.annotation.results, - deleteResult: item.deleteResult?.bind(item), - getValue: () => { - const result = item.annotation?.results.find((r) => r.from_name === item); - return result ? result.value : null; - }, - setValue: item.setValue?.bind(item), - updateResult: item.updateResult?.bind(item), - tagName: item.name, - toName: item.toname, - props: item.parsedProps, - }; - - function decodeHtmlEntities(text) { - const textArea = document.createElement("textarea"); - textArea.innerHTML = text; - return textArea.value; - } - - const transformedCode = decodeHtmlEntities(item.effectiveCode); - // const transformedCode = POC_UI; - - const UserComponent = () => { - const code = ` - "use strict"; - try { - const { - React, useState, regions, addRegion, deleteRegion, clearAllRegions, - state, saveState, metadata, saveMetadata, deleteMetadata, clearAllMetadata, - tags, getTagValue, setTagValue, item, annotation, store, task, - getValue, setValue, tagName, toName, props - } = arguments[0]; - - const userFunction = (${transformedCode}); - return userFunction(arguments[0]); - } catch (error) { - console.error('Error in custom component function:', error); - console.error('Original code:', ${JSON.stringify(item.effectiveCode)}); - console.error('Transformed code:', ${JSON.stringify(transformedCode)}); - throw error; - } - `; - const componentFunction = new Function(code); - return componentFunction(context); - }; - - setDynamicComponent(() => UserComponent); - setError(null); - } catch (err) { - console.group("🚨 CustomInterface Compilation Error"); - console.error("Error:", err); - console.error("Function Code:", item.effectiveCode); - console.error("Item:", item); - console.groupEnd(); - - setError(err); - setDynamicComponent(() => () => ( -
-

🚨 Component Compilation Error

-
- Error Message: -
-              {err?.toString() || "Unknown compilation error"}
-            
-
- {err?.stack && ( -
- 📍 Error Stack Trace -
-                {err.stack}
-              
-
- )} -
- 📝 Your Function Code -
-              {item.effectiveCode || "No code provided"}
-            
-
-
- 🔍 Context Information -
-

- Tag Name: {item?.name || "Unknown"} -

-

- To Name: {item?.toname || "Unknown"} -

-

- Data Loaded: {item?.dataLoaded ? "Yes" : "No"} -

-

- Data Error: {item?.dataError || "None"} -

-

- Has Code: {item.effectiveCode ? "Yes" : "No"} -

-

- Code Length: {item.effectiveCode?.length || 0} characters -

-

- Error Type: Compilation/Execution Error -

-
-
-
- 💡 Common Compilation Issues: -
    -
  • Syntax errors in JavaScript/JSX
  • -
  • Missing closing brackets or parentheses
  • -
  • Invalid JSX (check for properly closed tags)
  • -
  • Typos in parameter names or function syntax
  • -
  • Using variables that aren't in the context
  • -
  • Missing 'return' statement in your function
  • -
-
-
- )); - } - }, [item.effectiveCode, item.dataLoaded, item.dataError]); - - // --- CONDITIONAL RENDERING LOGIC HAPPENS AFTER ALL HOOKS --- - - if (!item.dataLoaded) { - return ( -
- Loading data... -
- ); - } - - const context = { - React, - useState: React.useState, - useEffect: React.useEffect, - useCallback: React.useCallback, - useMemo: React.useMemo, - useRef: React.useRef, - useReducer: React.useReducer, - useContext: React.useContext, - data: item.loadedData, - regions: item.regions, - addRegion: item.addRegion?.bind(item), - deleteRegion: item.remove?.bind(item), - clearAllRegions: item.perRegionCleanup?.bind(item), - state: item.globalState, - saveState: item.updateGlobalState?.bind(item), - saveData: item.updateGlobalState?.bind(item), - metadata: item.globalMetadata, - saveMetadata: item.addMetadata?.bind(item), - deleteMetadata: item.removeMetadata?.bind(item), - clearAllMetadata: item.clearAllMetadata?.bind(item), - tags: () => { - return Array.from(item.annotation.names.values()) - .filter((tag) => tag.type === "choices") - .map((tag) => { - const options = (tag.children || []).map((choice) => choice.value ?? choice._value); - return { - name: tag.name, - type: tag.type, - options, - }; - }); - }, - getTagValue: (tagName) => { - const tag = item.annotation.names.get(tagName); - if (!tag) return null; - const result = item.annotation.results.find((r) => r.from_name === tag); - return result ? result.value : null; - }, - setTagValue: ((tagName, value) => { - const tag = item.annotation.names.get(tagName); - if (!tag) return; - if (tag.type !== "choices") { - console.warn( - `POC LIMITATION: setTagValue only supports 'choices' tags, got '${tag.type}' for tag '${tagName}'`, - ); - alert(`POC Limitation: Only Choices tags are currently supported. Tag '${tagName}' is type '${tag.type}'.`); - return; - } - let formattedValue = value; - if (tag.type === "choices") { - formattedValue = Array.isArray(value) ? value : [value]; - } - const existingResult = item.annotation.results.find((r) => r.from_name === tag); - if (existingResult) { - if (existingResult.setValue) { - existingResult.setValue(formattedValue); - } else { - existingResult.value = formattedValue; - } - } else { - item.annotation.createResult({}, { [tag.valueType]: formattedValue }, tag, tag.toname); - } - if (typeof item.updateResult === "function") { - item.updateResult(); - } - }).bind(item), - item, - annotation: item.annotation, - store: item.annotation?.store || null, - task: item.annotation?.task || null, - getAllResults: () => item.annotation.results, - deleteResult: item.deleteResult?.bind(item), - getValue: () => { - const result = item.annotation?.results.find((r) => r.from_name === item); - return result ? result.value : null; - }, - setValue: item.setValue?.bind(item), - updateResult: item.updateResult?.bind(item), - tagName: item.name, - toName: item.toname, - props: item.parsedProps, - }; - - const wrapperStyle = { - ...item.parsedStyle, - }; - - const content = DynamicComponent ? :
Loading...
; - - return ( -
- {item.css &&