import React, { useEffect, useState } from 'react'; import { computeCoercion, getCurrentNodeType, hasDeepChildren, resolveReferences } from './utils'; import CodeHinter from '.'; import { copyToClipboard } from '@/_helpers/appUtils'; import { Alert } from '@/_ui/Alert/Alert'; import { Button } from '@/components/ui/Button/Button'; import _, { isEmpty } from 'lodash'; import { handleCircularStructureToJSON, hasCircularDependency, verifyConstant } from '@/_helpers/utils'; import Popover from 'react-bootstrap/Popover'; import Card from 'react-bootstrap/Card'; // eslint-disable-next-line import/no-unresolved import { JsonViewer } from '@textea/json-viewer'; import { reservedKeywordReplacer } from '@/_lib/reserved-keyword-replacer'; import useStore from '@/AppBuilder/_stores/store'; import { shallow } from 'zustand/shallow'; import { Overlay } from 'react-bootstrap'; import { ToolTip } from '@/_components/ToolTip'; import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext'; import { findDefault } from '../_utils/component-properties-validation'; import FixWithAi from './FixWithAi'; const sanitizeLargeDataset = (data, callback) => { const SIZE_LIMIT_KB = 5 * 1024; // 5 KB in bytes const estimateSizeOfObject = (object) => { const visited = new Set(); function sizeOf(obj) { if (obj === null || typeof obj !== 'object') return 0; if (visited.has(obj)) return 0; visited.add(obj); let bytes = 0; for (let key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { bytes += key.length * 2; const value = obj[key]; switch (typeof value) { case 'boolean': bytes += 4; break; case 'number': bytes += 8; break; case 'string': bytes += value.length * 2; break; case 'object': if (Array.isArray(value)) { bytes += value.reduce((acc, el) => acc + sizeOf(el), 0); } else { bytes += sizeOf(value); } break; } } } return bytes; } return sizeOf(object); }; function trimTo5KB(str) { let bytes = 0; let result = ''; for (let i = 0; i < str.length; i++) { const charCode = str.charCodeAt(i); const charBytes = charCode <= 0x7f ? 1 : 2; // basic approximation for UTF-16 if (bytes + charBytes > SIZE_LIMIT_KB) { break; } result += str[i]; bytes += charBytes; } return result + '...'; } const sanitize = (input) => { if (typeof input === 'string') return trimTo5KB(input); if (typeof input !== 'object' || input === null) return input; if (Array.isArray(input)) { const size = estimateSizeOfObject(input); callback(size > SIZE_LIMIT_KB); return data.length > 10 && size > SIZE_LIMIT_KB ? [input[0], `Too large to display: ${input.length - 1} more items`] : input; } else { const sanitizedData = Object.entries(input).reduce((acc, [key, value]) => { const sizeOfEachElement = estimateSizeOfObject(value); if (Array.isArray(value) && (data.length > 10 || sizeOfEachElement > SIZE_LIMIT_KB)) { acc[key] = [value[0], `Too large to display: ${value.length - 1} more items`]; } else { acc[key] = sanitize(value); } return acc; }, {}); return sanitizedData; } }; return sanitize(data); }; export const PreviewBox = ({ currentValue, validationSchema, setErrorStateActive, setErrorMessage, customVariables, isWorkspaceVariable, validationFn, }) => { const { moduleId } = useModuleContext(); const [resolvedValue, setResolvedValue] = useState(''); const [error, setError] = useState(null); const [coersionData, setCoersionData] = useState(null); const [largeDataset, setLargeDataset] = useState(false); const globals = useStore((state) => state.getAllExposedValues(moduleId).constants || {}, shallow); const secrets = useStore((state) => state.getSecrets(), shallow); const globalServerConstantsRegex = /\{\{.*globals\.server.*\}\}/; const getPreviewContent = (content, type) => { if (content === undefined || content === null) return currentValue; try { switch (type) { case 'Object': case 'Array': return JSON.stringify(content); case 'Boolean': return content.toString(); default: return content; } } catch (e) { return undefined; } }; let previewType = getCurrentNodeType(resolvedValue); let previewContent = resolvedValue; let isGlobalConstant = currentValue && currentValue.includes('{{constants.'); let isSecretConstant = currentValue && currentValue.includes('{{secrets.'); const isServerConstant = currentValue && currentValue.match(globalServerConstantsRegex); let invalidConstants = null; let undefinedError = null; if (isGlobalConstant || isSecretConstant) { invalidConstants = verifyConstant(currentValue, globals, secrets); } if (invalidConstants?.length) { undefinedError = { type: 'Invalid constants' }; } const ifCoersionErrorHasCircularDependency = (value) => { if (hasCircularDependency(value)) { return JSON.stringify(value, handleCircularStructureToJSON()); } return value; }; const content = getPreviewContent(previewContent, previewType); useEffect(() => { if (error) { setErrorStateActive(true); setErrorMessage(error); } else { setErrorStateActive(false); setErrorMessage(null); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [error]); useEffect(() => { const [valid, _error, rawNewValue, rawResolvedValue] = resolveReferences( currentValue, validationSchema, customVariables, validationFn ); const completeErrMessage = Array.isArray(_error) ? _error.join('.') : _error; const resolvedValue = typeof rawResolvedValue === 'function' ? undefined : rawResolvedValue; const newValue = typeof rawNewValue === 'function' ? undefined : rawNewValue; const isSecretError = currentValue?.includes('secrets.') || _error?.includes('ReferenceError: secrets is not defined'); if ((isWorkspaceVariable || !validationSchema || isEmpty(validationSchema)) && !validationFn) { return setResolvedValue(newValue); } // we dont need to add or update the resolved value if the value has deep children const _resolveValue = sanitizeLargeDataset(resolvedValue, setLargeDataset); if (valid && !isSecretError) { const [coercionPreview, typeAfterCoercion, typeBeforeCoercion] = computeCoercion(resolvedValue, newValue); setResolvedValue(_resolveValue); setCoersionData({ coercionPreview, typeAfterCoercion, typeBeforeCoercion, }); setError(null); } else if (!valid && !newValue && !resolvedValue && !isSecretError) { const err = !error ? `Invalid value for ${validationSchema?.schema?.type}` : `${_error}`; setError({ message: err, value: resolvedValue, type: 'Invalid', completeErrorMessage: completeErrMessage }); } else { const jsErrorType = isSecretError ? 'Error' : _error?.includes('ReferenceError') ? 'ReferenceError' : _error?.includes('TypeError') ? 'TypeError' : _error?.includes('SyntaxError') ? 'SyntaxError' : 'Invalid'; const errValue = ifCoersionErrorHasCircularDependency(_resolveValue); setError({ message: isServerConstant ? 'Server variables cannot be used in apps' : isSecretError ? 'secrets cannot be used in apps' : _error, value: isSecretError ? 'Undefined' : jsErrorType === 'Invalid' ? JSON.stringify(errValue, reservedKeywordReplacer) : resolvedValue, type: isSecretError ? 'Error' : jsErrorType, completeErrorMessage: completeErrMessage, }); setCoersionData(null); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [currentValue]); return ( <> copyToClipboard(error ? error?.value : content)} icon={'copy'} tip={'Copy to clipboard'} /> ); }; const RenderResolvedValue = ({ error, previewType, resolvedValue, coersionData, withValidation, isWorkspaceVariable, isSecretConstant = false, isServerConstant = false, isLargeDataset, }) => { const isServerSideGlobalResolveEnabled = useStore( (state) => !!state?.license?.featureAccess?.serverSideGlobalResolve, shallow ); const computeCoersionPreview = (resolvedValue, coersionData) => { if (coersionData?.typeBeforeCoercion === coersionData?.typeAfterCoercion) return resolvedValue; if (coersionData?.typeBeforeCoercion === 'array') { return '[...]' + coersionData?.coercionPreview; } if (coersionData?.typeBeforeCoercion === 'object') { return '{...}' + coersionData?.coercionPreview; } return resolvedValue + coersionData?.coercionPreview; }; const previewValueType = isWorkspaceVariable ? previewType : withValidation || (coersionData && coersionData?.typeBeforeCoercion) ? `${coersionData?.typeBeforeCoercion} ${ coersionData?.coercionPreview ? ` → ${coersionData?.typeAfterCoercion}` : '' }` : previewType; const previewContent = isServerConstant ? isServerSideGlobalResolveEnabled ? 'Server variables would be resolved at runtime' : 'Server variables are only available in paid plans' : isSecretConstant ? 'Values of secret constants are hidden' : !withValidation ? resolvedValue : computeCoersionPreview(resolvedValue, coersionData); const cls = error ? 'codehinter-error-banner' : 'codehinter-success-banner'; return (
{error ? error.type : previewValueType}
); }; function FixIssueTooltipContent() { return ( <>
Auto-fix

Diagnose and resolve errors instantly to keep your apps running smoothly

); } const PreviewContainer = ({ children, isFocused, enablePreview, setCursorInsidePreview, isPortalOpen, previewRef, showPreview, onAiSuggestionAccept, ...restProps }) => { const { validationSchema, isWorkspaceVariable, errorStateActive, previewPlacement, validationFn, componentId, paramName, fieldMeta, setIsFocused, currentValue, } = restProps; const aiFeaturesEnabled = useStore((state) => state.ai?.aiFeaturesEnabled ?? false); const fetchErrorFixUsingAi = useStore((state) => state.fetchErrorFixUsingAi); const clearChatHistory = useStore((state) => state.clearChatHistory); const componentDefinition = useStore((state) => state.getComponentDefinition(componentId), shallow); // TODO: check if moduleId needs to be passed here const componentName = componentDefinition?.component?.name; const componentKey = `${componentName} - ${fieldMeta?.displayName}`; const chatList = useStore((state) => state.fixWithAiSlice?.[componentId]?.[componentKey]?.chatHistory ?? []); const [errorMessage, setErrorMessage] = useState(null); const [popoverToShow, setPopoverToShow] = useState('preview'); // preview | fixWithAI const errMsg = errorMessage?.message ?? null; const typeofError = getCurrentNodeType(errMsg); const errorMsg = typeofError === 'Array' ? errMsg[0] : errMsg; const darkMode = localStorage.getItem('darkMode') === 'true'; useEffect(() => { !showPreview && setPopoverToShow('preview'); }, [showPreview]); useEffect(() => { setPopoverToShow('preview'); if (chatList?.length) { clearChatHistory(componentId, componentKey); } }, [currentValue]); const fetchFixUsingAi = () => { const defaultValue = validationSchema?.defaultValue ? validationSchema?.defaultValue : validationSchema ? findDefault(validationSchema?.schema ?? {}, errorMessage?.value) : undefined; const errorData = { key: componentKey, componentId: componentId, message: errorMessage?.completeErrorMessage, error: { resolvedProperty: { [paramName]: errorMessage?.value }, effectiveProperty: { [paramName]: defaultValue }, componentId, }, }; fetchErrorFixUsingAi(errorData, { componentDisplayName: componentDefinition?.component?.displayName ?? componentDefinition?.component?.component ?? componentName, errorPropertyDisplayName: fieldMeta?.displayName, customErrMessage: errorMessage?.message, }); }; const handleFixErrorWithAI = () => { setPopoverToShow('fixWithAI'); if (!componentId || chatList?.length) { return; } fetchFixUsingAi(); }; const fixWithAIPopover = ( setCursorInsidePreview(true)} onMouseLeave={() => setCursorInsidePreview(false)} > setIsFocused(false)} /> ); const popover = ( setCursorInsidePreview(true)} onMouseLeave={() => setCursorInsidePreview(false)} >
{errorStateActive && (
{errorMsg !== 'null' ? errorMsg : 'Invalid'}
{aiFeaturesEnabled && ( } tooltipClassName="[&_.tooltip-inner]:tw-text-left [&_.tooltip-inner]:tw-p-3" > )}
)} {!isEmpty(validationSchema) && ( <>
Expected
{validationSchema?.schema?.type}
)}
{!isEmpty(validationSchema) && (
Current
)}
{isWorkspaceVariable && }
); return ( <> {!isPortalOpen && ( { const popperEl = state?.elements?.popper; if (!popperEl || typeof IntersectionObserver === 'undefined') return; let rafId = null; const io = new IntersectionObserver( (entries) => { const ent = entries[0]; if (!ent) return; // intersectionRatio < 1 => partially/fully out of viewport if (ent.intersectionRatio < 1) { if (rafId) return; rafId = requestAnimationFrame(() => { try { instance.update(); } catch (e) { /* error */ } finally { rafId = null; } }); } }, { threshold: [0, 0.01, 0.5, 1] } ); io.observe(popperEl); return () => { io.disconnect(); if (rafId) cancelAnimationFrame(rafId); }; }, }, ], onFirstUpdate: (state) => { // Force position update on first render // This is done to avoid scroll issue if (state.elements.popper) { state.elements.popper.style.position = 'fixed'; } }, }} > {(props) => React.cloneElement(popoverToShow === 'fixWithAI' ? fixWithAIPopover : popover, props)} )} {children} ); }; const PreviewCodeBlock = ({ code, isExpectValue = false, isLargeDataset }) => { let preview; if (typeof code === 'string') { preview = code.trim(); } else if (typeof code === 'symbol') { preview = code.toString(); } else { preview = String(code); } const shouldTrim = preview.length > 35; let showJSONTree = false; if (isExpectValue && shouldTrim) { preview = preview.substring(0, 35) + '...' + preview.substring(preview.length - 2, preview.length); } let prettyPrintedJson = preview; try { prettyPrintedJson = JSON.parse(preview); const typeOfValue = typeof prettyPrintedJson; if (typeOfValue === 'object' || typeOfValue === 'array') { showJSONTree = true; } else { prettyPrintedJson = preview; showJSONTree = false; } } catch (e) { prettyPrintedJson = preview; showJSONTree = false; } if (showJSONTree) { const darkMode = localStorage.getItem('darkMode') === 'true'; const hasDeepChild = hasDeepChildren(prettyPrintedJson); return (
); } return (
        {prettyPrintedJson?.startsWith('{{') && prettyPrintedJson?.endsWith('{{')
          ? prettyPrintedJson?.replace(/{{/g, '').replace(/}}/g, '')
          : prettyPrintedJson}
      
); }; PreviewBox.RenderResolvedValue = RenderResolvedValue; PreviewBox.Container = PreviewContainer; PreviewBox.CodeBlock = PreviewCodeBlock;