import React, { useState, useEffect, useRef } from 'react'; import { GenericPageData, PageButton, PageContent, resolveLanguageText, SettingsFieldConfig, SettingsSectionConfig } from './pageInterface'; import { FormGenerator } from '../../components/FormGenerator'; import { FormGeneratorForm, AttributeDefinition } from '../../components/FormGenerator/FormGeneratorForm'; import { Button, UploadButton, CreateButton, TextField, Messages, ChatMessage, LogMessage, DropdownSelect, Log, WorkflowStatus } from '../../components/UiComponents'; import { Popup } from '../../components/UiComponents/Popup'; import { ConnectedFilesList } from '../../components/UiComponents/ConnectedFilesList'; import type { DropdownSelectItem } from '../../components/UiComponents/DropdownSelect'; import { DragDropOverlay } from '../../components/UiComponents/DragDropOverlay'; import { useLanguage } from '../../providers/language/LanguageContext'; import { usePermissions } from '../../hooks/usePermissions'; import { FiPaperclip } from 'react-icons/fi'; import styles from '../../styles/pages.module.css'; interface PageRendererProps { pageData: GenericPageData; onButtonClick?: (buttonId: string, button: PageButton) => void; } // Component wrapper to fix TextField height and prevent auto-grow const FixedHeightTextField: React.FC<{ value: string; onChange?: (value: string) => void; placeholder?: string; size?: 'sm' | 'md' | 'lg'; disabled?: boolean; className?: string; onKeyDown?: (e: React.KeyboardEvent) => void; }> = ({ value, onChange, placeholder, size, disabled, className, onKeyDown }) => { const wrapperRef = useRef(null); const textareaRef = useRef(null); useEffect(() => { // Override the TextField's auto-grow behavior by finding the textarea and setting fixed height const updateTextareaHeight = () => { if (wrapperRef.current) { // Find the textarea element const textarea = wrapperRef.current.querySelector('textarea') as HTMLTextAreaElement; if (textarea) { textareaRef.current = textarea; // Get the input container const inputContainer = wrapperRef.current.querySelector('.inputContainer') as HTMLElement; if (inputContainer) { // Get the computed height of the input container const containerRect = inputContainer.getBoundingClientRect(); const containerStyle = window.getComputedStyle(inputContainer); const paddingTop = parseFloat(containerStyle.paddingTop) || 0; const paddingBottom = parseFloat(containerStyle.paddingBottom) || 0; // Get textarea border width (top + bottom) const textareaStyle = window.getComputedStyle(textarea); const borderTop = parseFloat(textareaStyle.borderTopWidth) || 0; const borderBottom = parseFloat(textareaStyle.borderBottomWidth) || 0; const borderHeight = borderTop + borderBottom; // Calculate available height for textarea (subtract border to prevent clipping) const availableHeight = containerRect.height - paddingTop - paddingBottom - borderHeight; // Force the height - this will override TextField's auto-grow if (availableHeight > 0) { textarea.style.setProperty('height', `${availableHeight}px`, 'important'); textarea.style.setProperty('min-height', `${availableHeight}px`, 'important'); textarea.style.setProperty('max-height', `${availableHeight}px`, 'important'); textarea.style.setProperty('overflow-y', 'auto', 'important'); textarea.style.setProperty('resize', 'none', 'important'); textarea.style.setProperty('box-sizing', 'border-box', 'important'); textarea.style.setProperty('border-radius', '25px', 'important'); // Don't set box-shadow here - it should only appear on focus via CSS } } else { // Fallback: use wrapper height const wrapperHeight = wrapperRef.current.offsetHeight; if (wrapperHeight > 0) { textarea.style.setProperty('height', `${wrapperHeight}px`, 'important'); textarea.style.setProperty('min-height', `${wrapperHeight}px`, 'important'); textarea.style.setProperty('max-height', `${wrapperHeight}px`, 'important'); textarea.style.setProperty('overflow-y', 'auto', 'important'); textarea.style.setProperty('resize', 'none', 'important'); } } } } }; // Initial update after a short delay to ensure DOM is ready const timeoutId = setTimeout(() => { updateTextareaHeight(); }, 10); // Use ResizeObserver to watch for container size changes let resizeObserver: ResizeObserver | null = null; if (wrapperRef.current && window.ResizeObserver) { resizeObserver = new ResizeObserver(() => { updateTextareaHeight(); }); resizeObserver.observe(wrapperRef.current); } // Use MutationObserver to catch when TextField's useEffect runs and override it let mutationObserver: MutationObserver | null = null; if (wrapperRef.current) { mutationObserver = new MutationObserver(() => { updateTextareaHeight(); }); mutationObserver.observe(wrapperRef.current, { attributes: true, childList: true, subtree: true, attributeFilter: ['style', 'class'] }); } // Use an interval as a fallback to continuously override auto-grow const interval = setInterval(updateTextareaHeight, 100); // Also listen for resize events window.addEventListener('resize', updateTextareaHeight); return () => { clearTimeout(timeoutId); if (resizeObserver) { resizeObserver.disconnect(); } if (mutationObserver) { mutationObserver.disconnect(); } clearInterval(interval); window.removeEventListener('resize', updateTextareaHeight); }; }, [value]); return (
); }; // Component to handle async permission checks for content const ContentRenderer: React.FC<{ contents: PageContent[]; renderContent: (content: PageContent) => React.ReactNode; hasPermission: (context: 'DATA' | 'UI' | 'RESOURCE', item: string, operation?: 'read' | 'create' | 'update' | 'delete') => Promise; }> = ({ contents, renderContent, hasPermission }) => { const [visibleContents, setVisibleContents] = useState([]); const [loading, setLoading] = useState(true); useEffect(() => { const checkPermissions = async () => { setLoading(true); const visible: PageContent[] = []; for (const content of contents) { const contentId = content.id || `content-${content.type}`; let shouldRender = true; // Check RBAC permissions for content try { const hasRBACAccess = await hasPermission('UI', contentId, 'read'); if (!hasRBACAccess) { shouldRender = false; } } catch (error) { console.error(`Error checking RBAC access for content ${contentId}:`, error); shouldRender = false; } if (shouldRender) { visible.push(content); } } setVisibleContents(visible); setLoading(false); }; checkPermissions(); }, [contents, hasPermission]); if (loading) { return null; // Or return a loading indicator if desired } // Check if this is a dashboard layout pattern: messages, log, inputForm const hasMessages = visibleContents.some(c => c.type === 'messages'); const hasLog = visibleContents.some(c => c.type === 'log'); const hasInputForm = visibleContents.some(c => c.type === 'inputForm'); const isDashboardLayout = hasMessages && hasLog && hasInputForm; if (isDashboardLayout) { // Render dashboard grid layout const messagesContent = visibleContents.find(c => c.type === 'messages'); const logContent = visibleContents.find(c => c.type === 'log'); const inputFormContent = visibleContents.find(c => c.type === 'inputForm'); const otherContents = visibleContents.filter(c => c.type !== 'messages' && c.type !== 'log' && c.type !== 'inputForm' ); return ( <>
{/* Top row: Messages | Log */}
{messagesContent && (
{renderContent(messagesContent)}
)} {logContent && (
{renderContent(logContent)}
)}
{/* Bottom row: Input Form (which includes connected files) */} {inputFormContent && (
{renderContent(inputFormContent)}
)}
{/* Render any other content sections */} {otherContents.map((content, index) => ( {renderContent(content)} ))} ); } return ( <> {visibleContents.map((content, index) => ( {renderContent(content)} ))} ); }; const PageRenderer: React.FC = ({ pageData, onButtonClick }) => { // Get translation function from language context const { t } = useLanguage(); const { hasPermission } = usePermissions(); // Call the hook at the top level to ensure it persists across renders // This is CRITICAL - hooks must be called in the same order on every render const tableContent = pageData.content?.find(content => content.type === 'table'); const inputFormContent = pageData.content?.find(content => content.type === 'inputForm'); const settingsContent = pageData.content?.find(content => content.type === 'settings'); const hookFactory = tableContent?.tableConfig?.hookFactory || inputFormContent?.inputFormConfig?.hookFactory || settingsContent?.settingsConfig?.hookFactory; // Create a stable hook instance using React.useMemo // This ensures the same hook instance is used across re-renders const useTableData = React.useMemo(() => { if (hookFactory) { return hookFactory(); } return null; }, [hookFactory]); // Call the hook to get the current data // This will be called on every render, but it's the SAME hook instance const hookData = useTableData ? useTableData() : null; // Handle button clicks const handleButtonClick = async (button: PageButton) => { try { // Check RBAC permissions // Determine operation based on button type/action let operation: 'read' | 'create' | 'update' | 'delete' | undefined = undefined; if (button.id.includes('delete') || button.id.includes('remove')) { operation = 'delete'; } else if (button.id.includes('create') || button.id.includes('add') || button.id.includes('new')) { operation = 'create'; } else if (button.id.includes('edit') || button.id.includes('update') || button.id.includes('save')) { operation = 'update'; } else { operation = 'read'; } try { const hasRBACAccess = await hasPermission('UI', button.id, operation); if (!hasRBACAccess) { return; } } catch (error) { console.error(`Error checking RBAC access for button ${button.id}:`, error); return; } // Call the button's onClick handler with hook data if (button.onClick) { await button.onClick(hookData); } // Call the parent handler if (onButtonClick) { onButtonClick(button.id, button); } } catch (error) { console.error(`Error handling button click for ${button.id}:`, error); } }; // Helper function to get nested value using dot notation (generic utility) const getNestedValue = (obj: any, path: string): any => { return path.split('.').reduce((current, key) => current?.[key], obj); }; // Helper function to set nested value using dot notation (generic utility) const setNestedValue = (obj: any, path: string, value: any): any => { const keys = path.split('.'); const result = { ...obj }; let current = result; for (let i = 0; i < keys.length - 1; i++) { const key = keys[i]; if (!(key in current)) { current[key] = {}; } current[key] = { ...current[key] }; current = current[key]; } current[keys[keys.length - 1]] = value; return result; }; // Generic form section renderer - reusable for any form-based content const FormSectionRenderer: React.FC<{ sections: SettingsSectionConfig[]; formData: any; fieldsBySection: Record; loadingBySection: Record; errorsBySection: Record; onSave?: (sectionId: string, data: any) => Promise; getNestedValue: (obj: any, path: string) => any; setNestedValue: (obj: any, path: string, value: any) => any; }> = ({ sections, formData, fieldsBySection, loadingBySection, errorsBySection, onSave, getNestedValue, setNestedValue }) => { const [sectionFormData, setSectionFormData] = useState>({}); const [sectionSaveLoading, setSectionSaveLoading] = useState>({}); const [sectionSaveMessages, setSectionSaveMessages] = useState>({}); const formRefs = useRef>({}); // Initialize form data from formData when it changes useEffect(() => { const newFormData: Record = {}; sections.forEach(section => { const allFields = [ ...(section.staticFields || []), ...(fieldsBySection[section.sectionId] || []) ]; if (allFields.length === 0) return; const sectionData: any = { ...(sectionFormData[section.id] || {}) }; allFields.forEach(field => { const value = getNestedValue(formData, field.dataKey); if (value !== undefined && sectionData[field.dataKey] !== value) { sectionData[field.dataKey] = value; } }); newFormData[section.id] = sectionData; }); const hasChanges = Object.keys(newFormData).some(sectionId => { const newData = newFormData[sectionId]; const oldData = sectionFormData[sectionId] || {}; return JSON.stringify(newData) !== JSON.stringify(oldData); }); if (hasChanges) { setSectionFormData(prev => ({ ...prev, ...newFormData })); } }, [formData, sections.length, JSON.stringify(fieldsBySection)]); // Helper function to convert SettingsFieldConfig to AttributeDefinition const convertFieldToAttribute = (field: SettingsFieldConfig): AttributeDefinition => { // Determine the type based on field.type and inputType let attributeType: AttributeDefinition['type'] = 'text'; if (field.type === 'select') { attributeType = 'select'; } else if (field.type === 'toggle') { attributeType = 'boolean'; } else if (field.type === 'text' && field.inputType) { // Map inputType to attribute type if (field.inputType === 'email') { attributeType = 'email'; } else if (field.inputType === 'tel') { attributeType = 'text'; // tel is not a separate type in AttributeDefinition } else { attributeType = 'text'; } } // Convert options format if present let options: AttributeDefinition['options'] = undefined; if (field.options && field.options.length > 0) { options = field.options.map(opt => ({ value: opt.value, label: typeof opt.label === 'string' ? opt.label : resolveLanguageText(opt.label, t) })); } // Determine if field is disabled/readonly const isDisabled = typeof field.disabled === 'function' ? field.disabled(formData) : field.disabled || false; return { name: field.dataKey, type: attributeType, label: typeof field.label === 'string' ? field.label : resolveLanguageText(field.label, t), description: field.description ? (typeof field.description === 'string' ? field.description : resolveLanguageText(field.description, t)) : undefined, required: field.required || false, readonly: isDisabled, editable: !isDisabled, placeholder: field.placeholder ? (typeof field.placeholder === 'string' ? field.placeholder : resolveLanguageText(field.placeholder, t)) : undefined, options: options }; }; const handleSectionSave = async (section: typeof sections[0], formDataToSave?: any) => { // If formDataToSave is provided (from FormGeneratorForm onSubmit), use it // Otherwise, try to get it from the form ref or use current sectionFormData const dataToSave = formDataToSave || sectionFormData[section.id] || {}; setSectionSaveLoading(prev => ({ ...prev, [section.id]: true })); setSectionSaveMessages(prev => ({ ...prev, [section.id]: null })); try { if (onSave) { await onSave(section.id, dataToSave); } else if (section.onSave) { await section.onSave(section.id, dataToSave); } setSectionSaveMessages(prev => ({ ...prev, [section.id]: { type: 'success', text: t('settings.save_success') || 'Settings saved successfully' } })); setTimeout(() => { setSectionSaveMessages(prev => ({ ...prev, [section.id]: null })); }, 3000); } catch (error: any) { setSectionSaveMessages(prev => ({ ...prev, [section.id]: { type: 'error', text: error.message || t('settings.save_error') || 'Failed to save settings' } })); } finally { setSectionSaveLoading(prev => ({ ...prev, [section.id]: false })); } }; return (
{sections.map(section => { // Check if section has conditional rendering logic if (section.renderCondition) { const shouldRender = section.renderCondition(formData); if (!shouldRender) { // Render alternative content if provided if (section.renderAlternative) { return ( {section.renderAlternative(formData, t, resolveLanguageText)} ); } return null; } } const allFields = [ ...(section.staticFields || []), ...(fieldsBySection[section.sectionId] || []) ]; const isLoading = loadingBySection[section.sectionId] || false; const error = errorsBySection[section.sectionId]; const saveLoading = sectionSaveLoading[section.id] || false; const saveMessage = sectionSaveMessages[section.id]; // Convert fields to AttributeDefinition format const attributes: AttributeDefinition[] = allFields.map(convertFieldToAttribute); // Get current form data for this section const currentSectionFormData = sectionFormData[section.id] || {}; // Prepare section form data (flatten nested structure for FormGeneratorForm) const formDataForSection: Record = {}; allFields.forEach(field => { const value = currentSectionFormData[field.dataKey] !== undefined ? currentSectionFormData[field.dataKey] : getNestedValue(formData, field.dataKey); if (value !== undefined) { formDataForSection[field.dataKey] = value; } }); return (
{/* Section Header */}
{section.icon && ( )}

{resolveLanguageText(section.title, t)}

{section.description && (

{resolveLanguageText(section.description, t)}

)}
{/* Loading State */} {isLoading && (
{t('common.loading')}
)} {/* Error State */} {error && !isLoading && (
{error}
)} {/* FormGeneratorForm */} {!isLoading && !error && attributes.length > 0 && ( <>
{ if (el) { const form = el.querySelector('form'); if (form) { formRefs.current[section.id] = form; } } }}> { // Update local section form data setSectionFormData(prev => ({ ...prev, [section.id]: { ...prev[section.id], ...formDataToSave } })); // Trigger save with form data await handleSectionSave(section, formDataToSave); }} showButtons={false} className="" />
{/* Save Message */} {saveMessage && (
{saveMessage.text}
)} {/* Save Button */}
)}
); })}
); }; // Render content based on type const renderContent = (content: PageContent) => { switch (content.type) { case 'heading': const HeadingTag = `h${content.level || 2}` as keyof React.JSX.IntrinsicElements; return React.createElement( HeadingTag, { key: content.id, className: styles.contentHeading }, resolveLanguageText(content.content, t) ); case 'paragraph': return (

{resolveLanguageText(content.content, t)}

); case 'list': return (
{content.content && (

{resolveLanguageText(content.content, t)}

)}
    {content.items?.map((item, index) => (
  • {resolveLanguageText(item, t)}
  • ))}
); case 'code': return (
                        
                            {resolveLanguageText(content.content, t)}
                        
                    
); case 'divider': return
; case 'custom': if (content.customComponent) { const CustomComponent = content.customComponent; return ; } return null; case 'table': if (content.tableConfig && hookData) { const { columns: configColumns, actionButtons, ...tableProps } = content.tableConfig; // Only show loading spinner on initial load (when there's no data yet) // During refetch, keep the existing data visible const showLoadingSpinner = hookData.loading && hookData.data.length === 0; // Show error state if there's an error if (hookData.error) { return (

Error loading data: {hookData.error}

{hookData.refetch && ( )}
); } // Use columns from hook data if available, otherwise use config columns // CRITICAL: Preserve columns even when data is empty (e.g., after filtering) // Columns from attributes should persist regardless of data state const hookColumns = hookData.columns && hookData.columns.length > 0 ? hookData.columns : undefined; const configCols = configColumns && configColumns.length > 0 ? configColumns : undefined; // Prioritize hookColumns (from attributes) over configColumns to ensure persistence const columns = hookColumns || configCols; // CRITICAL: Resolve LanguageText objects in column labels // Only map if columns exist, otherwise FormGenerator will auto-detect const resolvedColumns = columns ? columns.map(col => ({ ...col, label: resolveLanguageText(col.label, t) })) : undefined; // Convert action buttons to FormGenerator format // Filter out buttons that should be hidden based on RBAC permissions const formGeneratorActions = actionButtons?.filter(action => { // Check if button should be hidden based on permissions const permissions = (hookData as any)?.permissions; if (!permissions) { // If no permissions loaded yet, show button (will be filtered later) return true; } // Determine which permission to check based on button type let requiredPermission: 'read' | 'create' | 'update' | 'delete' | null = null; if (action.type === 'view' || action.type === 'play') { requiredPermission = 'read'; } else if (action.type === 'edit') { requiredPermission = 'update'; } else if (action.type === 'copy') { // Copy creates a new item, so it requires 'create' permission requiredPermission = 'create'; } else if (action.type === 'delete') { requiredPermission = 'delete'; } // If no specific permission required, show button if (!requiredPermission) { return true; } // Check if user has the required permission (not 'n') const hasPermission = permissions[requiredPermission] !== 'n' && permissions.view; // Log permission check for debugging if (import.meta.env.DEV) { console.log(`🔐 Permission check for ${action.type} button:`, { requiredPermission, hasPermission, permissionValue: permissions[requiredPermission], view: permissions.view }); } return hasPermission; }).map(action => { // Wrap disabled function to handle both row-based and hookData-based disabled functions let disabledFn: ((row: any) => boolean | { disabled: boolean; message?: string }); if (action.disabled) { if (typeof action.disabled === 'function') { // Try to call with hookData first (for permission-based checks) // If that works, use the result for all rows // Otherwise, fall back to calling with row try { // Check if function signature suggests it takes hookData // We'll try calling it with hookData - if it's designed for that, it will work const testCall = (action.disabled as any)(hookData); if (testCall !== undefined && (typeof testCall === 'boolean' || (typeof testCall === 'object' && 'disabled' in testCall))) { // Function accepts hookData - use result for all rows disabledFn = () => testCall; } else { // Function doesn't work with hookData, use row-based approach disabledFn = action.disabled as (row: any) => boolean | { disabled: boolean; message?: string }; } } catch { // Function doesn't accept hookData, use row-based approach disabledFn = action.disabled as (row: any) => boolean | { disabled: boolean; message?: string }; } } else { // Non-function disabled value disabledFn = () => action.disabled as boolean | { disabled: boolean; message?: string }; } } else { disabledFn = () => false; } return { type: action.type, onAction: action.onAction, // CRITICAL: Resolve LanguageText objects in action titles title: resolveLanguageText(action.title, t), isProcessing: action.loading || (() => false), disabled: disabledFn, // Preserve field mappings and operation names idField: action.idField, nameField: action.nameField, typeField: action.typeField, contentField: action.contentField, operationName: action.operationName, loadingStateName: action.loadingStateName, // Navigation and behavior (for play button) navigateTo: action.navigateTo, mode: action.mode }; }) || []; // Debug logging for table rendering if (import.meta.env.DEV) { console.log('🔍 Rendering FormGenerator:', { dataLength: hookData.data?.length || 0, columnsCount: resolvedColumns?.length || 0, loading: showLoadingSpinner, hasError: !!hookData.error, data: hookData.data, willAutoDetect: !resolvedColumns }); } return (
{hookData.isRefetching && (
Refreshing...
)}
); } return null; case 'inputForm': if (content.inputFormConfig && hookData) { const config = content.inputFormConfig; const isRunning = hookData.isRunning || false; // Determine button props based on workflow state const buttonLabel = isRunning ? (config.stopButtonLabel || config.buttonLabel) : config.buttonLabel; const buttonIcon = isRunning ? (config.stopButtonIcon || config.buttonIcon) : config.buttonIcon; const buttonVariant = isRunning ? (config.stopButtonVariant || config.buttonVariant || 'primary') : (config.buttonVariant || 'primary'); const buttonDisabled = hookData.isSubmitting || (!isRunning && !hookData.inputValue?.trim()); // Handle Enter key press const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey && hookData.handleSubmit && !hookData.isSubmitting) { e.preventDefault(); hookData.handleSubmit(); } }; // Check if we have file management const hasFileManagement = !!(hookData.handleFileUpload && hookData.workflowFiles !== undefined); // Check RBAC permissions for prompt selector and workflow mode selector // Show prompt selector if user has permission to view/read prompts (even if no prompts exist yet) const showPromptSelector = hookData.promptPermission && hookData.promptPermission.view !== false && hookData.promptPermission.read !== 'n'; const showWorkflowModeSelector = hookData.workflowModeItems !== undefined; // Calculate number of visible elements for equal width distribution const visibleElementsCount = (showPromptSelector ? 1 : 0) + (showWorkflowModeSelector ? 1 : 0) + 2; // Attach Files and Send buttons are always visible // Grid layout for pages with file management if (hasFileManagement) { return (
{/* Top row: Dropdown selectors and buttons */}
{showPromptSelector && (
)} {showWorkflowModeSelector && (
)}
{/* Bottom row: Input text area */}
{/* Right column: Connected Files List (spans both rows) */}
{/* File Attachment Popup */} {hookData.isFileAttachmentPopupOpen && ( hookData.setIsFileAttachmentPopupOpen?.(false)} size="large" >
{/* Upload Button Section */}
Upload New File
{/* Files List */}
{hookData.allUserFiles && hookData.allUserFiles.length > 0 ? (
{hookData.allUserFiles.map((file: any) => { const isAttached = hookData.pendingFiles?.some((pf: any) => pf.fileId === file.id); return (
hookData.handleFileAttach?.(file.id)} style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '0.75rem', border: `1px solid ${isAttached ? '#4CAF50' : '#e0e0e0'}`, borderRadius: '4px', cursor: 'pointer', backgroundColor: isAttached ? '#f1f8f4' : 'transparent', transition: 'all 0.2s' }} onMouseEnter={(e) => { if (!isAttached) { e.currentTarget.style.backgroundColor = '#f5f5f5'; } }} onMouseLeave={(e) => { if (!isAttached) { e.currentTarget.style.backgroundColor = 'transparent'; } }} >
📎
{file.file_name || file.fileName || 'Unknown File'}
{(() => { const size = file.size; const mimeType = file.mime_type || file.mimeType || 'application/octet-stream'; let sizeStr = ''; if (size) { if (size < 1024) { sizeStr = `${size} B`; } else if (size < 1024 * 1024) { sizeStr = `${(size / 1024).toFixed(1)} KB`; } else if (size < 1024 * 1024 * 1024) { sizeStr = `${(size / (1024 * 1024)).toFixed(1)} MB`; } else { sizeStr = `${(size / (1024 * 1024 * 1024)).toFixed(1)} GB`; } } return sizeStr ? `${sizeStr} • ${mimeType}` : mimeType; })()}
{isAttached ? 'Attached' : 'Attach'}
); })}
) : (
No files uploaded yet. Click "Upload New File" to add files.
)}
)}
); } // Default layout without files (backward compatible) return (
{/* Dropdown selectors row */} {(showPromptSelector || showWorkflowModeSelector) && (
{showPromptSelector && ( )} {showWorkflowModeSelector && ( )}
)} {/* Input and button row */}
); } return null; case 'messages': const config = content.messagesConfig || {}; const dataSource = config.dataSource || 'messages'; const hookMessages = Array.isArray(hookData?.messages) ? hookData.messages : []; const hookLogs = Array.isArray(hookData?.logs) ? hookData.logs : []; // Determine which data to render based on dataSource let messagesToRender: any[] = []; let variantToUse = config.variant || 'chat'; if (dataSource === 'logs') { messagesToRender = hookLogs; // Logs should use log variant by default variantToUse = config.variant || 'log'; } else if (dataSource === 'both') { // Merge messages and logs, with logs using log variant // We'll render them separately in sequence const combined = [ ...hookMessages.map((msg: any) => ({ ...msg, _renderVariant: config.variant || 'chat' })), ...hookLogs.map((log: any) => ({ ...log, _renderVariant: 'log' })) ].sort((a, b) => { // Sort by timestamp/sequence to interleave properly const aTime = a.publishedAt || a.timestamp || 0; const bTime = b.publishedAt || b.timestamp || 0; return aTime - bTime; }); messagesToRender = combined; // When both, we'll use a custom renderer } else { // Default: use messages messagesToRender = hookMessages; } // Custom renderer for when dataSource is 'both' to handle different variants const renderMessage = dataSource === 'both' ? (message: any, index: number) => { const variant = message._renderVariant || 'chat'; const cleanMessage = { ...message }; delete cleanMessage._renderVariant; if (variant === 'log') { return ( ); } else { return ( ); } } : undefined; return (
); case 'settings': if (content.settingsConfig && hookData) { const config = content.settingsConfig; const sections = config.sections; const formData = (hookData as any).settingsData || {}; const fieldsBySection = (hookData as any).settingsFields || {}; const loadingBySection = (hookData as any).settingsLoading || {}; const errorsBySection = (hookData as any).settingsErrors || {}; const saveSectionHandler = (hookData as any).saveSection; // Use a generic form renderer component that can be reused return ; } return null; case 'log': { const logConfig = content.logConfig || {}; const dashboardTree = hookData?.dashboardTree; const onToggleOperationExpanded = hookData?.onToggleOperationExpanded; const getChildOperations = hookData?.getChildOperations; return (
); } default: return null; } }; // Create enhanced drag drop config with hook data integration const getDragDropConfig = () => { if (!pageData.dragDropConfig) { return { enabled: false, onDrop: () => {} }; } // If the page has drag drop config and hook data with handleUpload, integrate them if (hookData?.handleUpload) { return { ...pageData.dragDropConfig, onDrop: async (files: File[]) => { try { // Process each file through the hook's handleUpload function for (const file of files) { if (hookData.handleUpload) { await hookData.handleUpload(file); } } } catch (error) { console.error('Error uploading dropped files:', error); } } }; } // Fallback to the original config return pageData.dragDropConfig; }; return (
{/* Page Header */}

{resolveLanguageText(pageData.title, t)}

{pageData.subtitle && (

{resolveLanguageText(pageData.subtitle, t)}

)}
{/* Header Buttons */} {pageData.headerButtons && pageData.headerButtons.length > 0 && (
{/* Workflow Status - Left of workflow selector */} {hookData && pageData.headerButtons.some(btn => btn.id === 'workflow-selector') && (
)} {pageData.headerButtons.filter((button) => { // Filter header buttons based on RBAC permissions // Check DATA permissions for create buttons if (button.id.includes('create') || button.id.includes('add') || button.id.includes('new')) { const permissions = (hookData as any)?.permissions; if (!permissions) { // If permissions not loaded yet, show button (will be filtered when permissions load) return true; } const hasCreate = permissions.create !== 'n' && permissions.view; // Log permission check for debugging if (import.meta.env.DEV) { console.log(`🔐 Header button permission check (${button.id}):`, { operation: 'create', hasPermission: hasCreate, permissionValue: permissions.create, view: permissions.view, fullPermissions: permissions }); } return hasCreate; } // For other buttons, show them (they may have their own permission checks) return true; }).map((button) => { // Check if this is a dropdown button if (button.dropdownConfig) { const dropdownConfig = button.dropdownConfig; // Get dropdown data from hookData if dataSource is specified let items: DropdownSelectItem[] = []; let selectedItemId: string | number | null = null; let onSelectHandler: (item: DropdownSelectItem | null) => void | Promise = () => {}; if (dropdownConfig.dataSource && hookData) { // Get items from hookData if (dropdownConfig.dataSource.itemsProperty) { const hookItems = (hookData as any)[dropdownConfig.dataSource.itemsProperty]; if (Array.isArray(hookItems)) { items = hookItems.map((item: any) => ({ id: item.id, label: typeof item.label === 'string' ? item.label : resolveLanguageText(item.label, t), value: item.value || item, metadata: item.metadata })); } } // Get selectedItemId from hookData if (dropdownConfig.dataSource.selectedIdProperty) { selectedItemId = (hookData as any)[dropdownConfig.dataSource.selectedIdProperty] || null; } // Get onSelect handler from hookData if (dropdownConfig.dataSource.onSelectMethod) { const hookOnSelect = (hookData as any)[dropdownConfig.dataSource.onSelectMethod!]; if (typeof hookOnSelect === 'function') { onSelectHandler = hookOnSelect; } } } else { // Use dropdownConfig directly items = dropdownConfig.items.map(item => ({ id: item.id, label: typeof item.label === 'string' ? item.label : resolveLanguageText(item.label, t), value: item.value, metadata: item.metadata })); selectedItemId = dropdownConfig.selectedItemId ?? null; onSelectHandler = (item: DropdownSelectItem | null) => { dropdownConfig.onSelect(item, hookData); }; } // Check if loading state is available from hookData // Use generic loading property or check for specific loading property from dropdownConfig const isLoading = dropdownConfig.dataSource?.loadingProperty ? (hookData as any)?.[dropdownConfig.dataSource.loadingProperty] || false : hookData?.loading || false; return ( ); } // Check if this is an upload button (has handleUpload in hookData) const handleUpload = (hookData as any)?.handleUpload; if (import.meta.env.DEV && button.id === 'upload-file') { console.log('🔍 Upload button check:', { buttonId: button.id, hasHandleUpload: !!handleUpload, hookDataKeys: hookData ? Object.keys(hookData) : 'no hookData', handleUploadType: typeof handleUpload }); } if (handleUpload && button.id === 'upload-file') { // Evaluate disabled function if it's a function let isDisabled = false; if (button.disabled !== undefined) { if (typeof button.disabled === 'function') { try { const disabledResult = (button.disabled as any)(hookData); if (typeof disabledResult === 'object' && disabledResult !== null && 'disabled' in disabledResult) { isDisabled = disabledResult.disabled; } else if (typeof disabledResult === 'boolean') { isDisabled = disabledResult; } } catch (error) { console.error(`Error evaluating disabled function for button ${button.id}:`, error); isDisabled = false; } } else if (typeof button.disabled === 'boolean') { isDisabled = button.disabled; } } return ( {resolveLanguageText(button.label, t)} ); } // Check if this button has a formConfig (create button) if (button.formConfig && hookData) { const createOperation = button.formConfig.createOperationName ? (hookData as any)[button.formConfig.createOperationName] : null; if (createOperation) { // Use generateEditFieldsFromAttributes from backend (required, no fallback) const hookDataAny = hookData as any; if (!hookDataAny.generateEditFieldsFromAttributes || typeof hookDataAny.generateEditFieldsFromAttributes !== 'function') { console.error('Create button requires generateEditFieldsFromAttributes function in hookData'); return null; } // Use dynamic fields from backend attributes const generatedFields = hookDataAny.generateEditFieldsFromAttributes(); if (!generatedFields || generatedFields.length === 0) { console.error('No fields generated from backend attributes'); return null; } // Resolve language text for generated fields const fieldsToUse = generatedFields.map((field: any) => ({ ...field, label: resolveLanguageText(field.label, t), placeholder: field.placeholder ? resolveLanguageText(field.placeholder, t) : undefined })); // Create a wrapper for onCreate that ensures attributes are loaded const wrappedCreateOperation = async (formData: any) => { // Ensure attributes are loaded before creating (if function exists) if (hookDataAny.ensureAttributesLoaded && typeof hookDataAny.ensureAttributesLoaded === 'function') { await hookDataAny.ensureAttributesLoaded(); } return await createOperation(formData); }; return ( { // Refetch data after successful creation if (hookData.refetch) { hookData.refetch(); } }} > {resolveLanguageText(button.label, t)} ); } } // Regular button // Evaluate disabled function if it's a function let isDisabled = false; if (button.disabled !== undefined) { if (typeof button.disabled === 'function') { try { const disabledResult = (button.disabled as any)(hookData); if (typeof disabledResult === 'object' && disabledResult !== null && 'disabled' in disabledResult) { isDisabled = disabledResult.disabled; } else if (typeof disabledResult === 'boolean') { isDisabled = disabledResult; } } catch (error) { console.error(`Error evaluating disabled function for button ${button.id}:`, error); isDisabled = false; } } else if (typeof button.disabled === 'boolean') { isDisabled = button.disabled; } } return ( ); })}
)}
{/* Page Content */}
{/* Message Overlay Component */} {hookData?.MessageOverlayComponent && }
); }; export default PageRenderer;