import { useState, useEffect, useCallback, useMemo, useRef } from 'react'; import { useApiRequest } from '../useApi'; import { useWorkflowSelection } from '../../contexts/WorkflowSelectionContext'; import { useFileContext } from '../../contexts/FileContext'; import { MessageDocument } from '../../components/UiComponents/Messages/MessagesTypes'; import { usePrompts } from '../usePrompts'; import { usePermissions } from '../usePermissions'; import { deleteFileFromMessageApi } from '../../api/workflowApi'; import type { Workflow, WorkflowMessage } from '../../api/workflowApi'; import { useWorkflowLifecycle } from './useWorkflowLifecycle'; import { useWorkflows } from './useWorkflows'; import { useDashboardLogTree } from './useDashboardLogTree'; import { extractFileIdsFromMessage, convertFilesToDocuments, sortMessages } from './playgroundUtils'; import type { WorkflowLog as LogTypesWorkflowLog } from '../../components/UiComponents/Log/LogTypes'; export interface WorkflowFile { id: string; fileId: string; fileName: string; fileSize: number; mimeType: string; messageId?: string; source?: 'user_uploaded' | 'ai_created'; } export function useDashboardInputForm() { const [inputValue, setInputValue] = useState(''); const [pendingFiles, setPendingFiles] = useState([]); const [isFileAttachmentPopupOpen, setIsFileAttachmentPopupOpen] = useState(false); const [optimisticMessage, setOptimisticMessage] = useState(null); const [selectedPromptId, setSelectedPromptId] = useState(null); const [workflowMode, setWorkflowMode] = useState<'Dynamic' | 'Automation' | null>(null); const { checkPermission, canView } = usePermissions(); const [playgroundUIPermission, setPlaygroundUIPermission] = useState(false); const [chatWorkflowPermission, setChatWorkflowPermission] = useState(null); const [promptPermission, setPromptPermission] = useState(null); const [filePermission, setFilePermission] = useState(null); const { selectedWorkflowId, selectWorkflow: selectWorkflowFromContext, clearWorkflow: clearWorkflowFromContext } = useWorkflowSelection(); const { workflowId, workflowStatus, currentRound, isRunning, isStopping, startingWorkflow, messages, dashboardLogs, unifiedContentLogs, latestStats, startWorkflow, stopWorkflow, resetWorkflow, selectWorkflow, setWorkflowStatusOptimistic } = useWorkflowLifecycle(); // Dashboard log tree hook const { tree: dashboardTree, processDashboardLogs, clearDashboard, toggleOperationExpanded, updateCurrentRound, getChildOperations } = useDashboardLogTree(); // Ref to prevent infinite sync loops const isSyncingRef = useRef(false); const fileContext = useFileContext(); const { request } = useApiRequest(); const { prompts, loading: promptsLoading, permissions: promptsPermissions, fetchPromptById } = usePrompts(); useEffect(() => { if (promptsPermissions) { setPromptPermission(promptsPermissions); } }, [promptsPermissions]); useEffect(() => { const checkPermissions = async () => { try { const uiPerm = await canView('UI', 'playground'); setPlaygroundUIPermission(uiPerm); if (uiPerm) { const chatWorkflowPerm = await checkPermission('DATA', 'ChatWorkflow'); setChatWorkflowPermission(chatWorkflowPerm); const promptPerm = await checkPermission('DATA', 'Prompt'); setPromptPermission(promptPerm); const filePerm = await checkPermission('DATA', 'FileItem'); setFilePermission(filePerm); } } catch (error) { } }; checkPermissions(); }, [canView, checkPermission]); // Sync context -> lifecycle: When context selection changes, update lifecycle useEffect(() => { if (isSyncingRef.current) return; if (selectedWorkflowId && selectedWorkflowId !== workflowId) { isSyncingRef.current = true; selectWorkflow(selectedWorkflowId).finally(() => { isSyncingRef.current = false; }); } else if (!selectedWorkflowId && workflowId) { // If context is cleared but lifecycle still has a workflow, reset lifecycle isSyncingRef.current = true; resetWorkflow(); isSyncingRef.current = false; } }, [selectedWorkflowId, workflowId, selectWorkflow, resetWorkflow]); // Sync lifecycle -> context: When lifecycle workflowId changes, update context useEffect(() => { if (isSyncingRef.current) return; if (workflowId && workflowId !== selectedWorkflowId) { isSyncingRef.current = true; selectWorkflowFromContext(workflowId); isSyncingRef.current = false; } else if (!workflowId && selectedWorkflowId) { // If lifecycle is cleared but context still has selection, clear context isSyncingRef.current = true; clearWorkflowFromContext(); isSyncingRef.current = false; } }, [workflowId, selectedWorkflowId, selectWorkflowFromContext, clearWorkflowFromContext]); useEffect(() => { const handleSetInput = (event: CustomEvent<{ value: string }>) => { const newValue = event.detail.value; if (newValue && typeof newValue === 'string') { setInputValue(newValue); } }; window.addEventListener('dashboardSetInput', handleSetInput as EventListener); return () => { window.removeEventListener('dashboardSetInput', handleSetInput as EventListener); }; }, []); const { workflows, loading: workflowsLoading, refetch: refetchWorkflows } = useWorkflows(); // Track processed log IDs to avoid reprocessing const processedLogIdsRef = useRef>(new Set()); const lastWorkflowIdRef = useRef(null); const lastDashboardLogsLengthRef = useRef(0); // Clear processed logs when workflow changes useEffect(() => { if (workflowId !== lastWorkflowIdRef.current) { processedLogIdsRef.current.clear(); lastWorkflowIdRef.current = workflowId || null; lastDashboardLogsLengthRef.current = 0; if (!workflowId) { clearDashboard(true); } } }, [workflowId, clearDashboard]); // Process dashboard logs when they change (only new logs) useEffect(() => { if (!dashboardLogs || dashboardLogs.length === 0) { lastDashboardLogsLengthRef.current = 0; return; } // Only process if the array length changed (indicating new logs) if (dashboardLogs.length === lastDashboardLogsLengthRef.current) { return; } // Filter to only new logs that haven't been processed const newLogs = dashboardLogs.filter(log => { const logId = log.id || `${log.operationId}-${log.timestamp}`; if (processedLogIdsRef.current.has(logId)) { return false; } processedLogIdsRef.current.add(logId); return true; }); // Only process if there are new logs if (newLogs.length > 0) { // Convert API WorkflowLog format to LogTypes WorkflowLog format const convertedLogs: LogTypesWorkflowLog[] = newLogs.map(log => ({ id: log.id || `${log.operationId || 'unknown'}-${log.timestamp || Date.now()}`, workflowId: log.workflowId || '', message: log.message || '', type: log.type, timestamp: log.timestamp || Date.now(), status: log.status, progress: log.progress, performance: log.performance, parentId: log.parentId, operationId: log.operationId })); processDashboardLogs(convertedLogs); } lastDashboardLogsLengthRef.current = dashboardLogs.length; }, [dashboardLogs, processDashboardLogs]); // Update current round in dashboard tree when it changes useEffect(() => { if (currentRound !== undefined) { updateCurrentRound(currentRound); } }, [currentRound, updateCurrentRound]); const workflowFiles = useMemo(() => { const fileMap = new Map(); const pendingFileIds = new Set(pendingFiles.map(f => f.fileId)); const addFilesFromMessage = (message: WorkflowMessage, messageId: string) => { const documents = (message as any).documents as MessageDocument[] | undefined; const files = (message as any).files as any[] | undefined; if (documents && Array.isArray(documents)) { documents.forEach((doc: MessageDocument) => { if (!doc.fileId || doc.fileId.trim() === '') return; if (!fileMap.has(doc.fileId)) { const source = pendingFileIds.has(doc.fileId) ? 'user_uploaded' : 'ai_created'; fileMap.set(doc.fileId, { id: doc.id || doc.fileId, fileId: doc.fileId, fileName: doc.fileName || 'Unknown File', fileSize: doc.fileSize || 0, mimeType: doc.mimeType || 'application/octet-stream', messageId: doc.messageId || messageId, source }); } }); } if (files && Array.isArray(files)) { files.forEach((file: any) => { const fileId = file.id || file.fileId; if (!fileId || fileId.trim() === '') return; if (!fileMap.has(fileId)) { const source = pendingFileIds.has(fileId) ? 'user_uploaded' : 'ai_created'; fileMap.set(fileId, { id: fileId, fileId: fileId, fileName: file.fileName || file.name || 'Unknown File', fileSize: file.fileSize || file.size || 0, mimeType: file.mimeType || file.mime_type || 'application/octet-stream', messageId: messageId, source }); } }); } }; if (messages && messages.length > 0) { messages.forEach((message: WorkflowMessage) => { addFilesFromMessage(message, message.id); }); } if (optimisticMessage) { addFilesFromMessage(optimisticMessage, optimisticMessage.id || 'optimistic'); } return Array.from(fileMap.values()); }, [messages, pendingFiles, optimisticMessage]); useEffect(() => { if (!messages || messages.length === 0) return; const messageTexts = new Set(); messages.forEach((message: WorkflowMessage) => { if (message.message) { messageTexts.add(message.message.trim()); } }); if (optimisticMessage && optimisticMessage.message) { const optimisticText = optimisticMessage.message.trim(); const optimisticFileIds = extractFileIdsFromMessage(optimisticMessage); const matchingMessage = Array.from(messages).find((msg: WorkflowMessage) => msg.message && msg.message.trim() === optimisticText ); if (matchingMessage) { const matchingFileIds = extractFileIdsFromMessage(matchingMessage); if (optimisticFileIds.size > 0) { const allFilesConfirmed = Array.from(optimisticFileIds).every(fileId => matchingFileIds.has(fileId) ); if (allFilesConfirmed && matchingFileIds.size > 0) { setOptimisticMessage(null); } } else { if (messageTexts.has(optimisticText)) { setOptimisticMessage(null); } } } } }, [messages, optimisticMessage]); const displayMessages = useMemo(() => { const optimisticText = optimisticMessage?.message?.trim(); const processedMessages = (messages || []).map((message: WorkflowMessage) => { const files = (message as any).files as any[] | undefined; const documents = (message as any).documents as MessageDocument[] | undefined; if (files && Array.isArray(files) && (!documents || documents.length === 0)) { return { ...message, documents: convertFilesToDocuments(files, message.id) }; } return message; }); let replacedMessageTimestamp: number | undefined; const filteredMessages = processedMessages.filter((message: WorkflowMessage) => { const isUserMessage = message.role?.toLowerCase() === 'user'; const messageText = message.message?.trim(); if (optimisticMessage && optimisticText && isUserMessage && messageText === optimisticText) { const documents = (message as any).documents as MessageDocument[] | undefined; const files = (message as any).files as any[] | undefined; const hasDocuments = documents && Array.isArray(documents) && documents.length > 0; const hasFiles = files && Array.isArray(files) && files.length > 0; if (hasDocuments || hasFiles) { return true; } if (message.publishedAt !== undefined) { replacedMessageTimestamp = message.publishedAt; } return false; } return true; }); const allMessages = [...filteredMessages]; if (optimisticMessage) { const optimisticWithTimestamp = replacedMessageTimestamp !== undefined ? { ...optimisticMessage, publishedAt: replacedMessageTimestamp } : optimisticMessage; allMessages.push(optimisticWithTimestamp); } return allMessages.sort(sortMessages); }, [messages, optimisticMessage, workflowId]); const handleFileUpload = useCallback(async (file: File): Promise<{ success: boolean; data: any }> => { const result = await fileContext.handleFileUpload(file, workflowId || undefined); if (result.success && result.fileData) { const responseData = result.fileData; const fileData = responseData.file || responseData; const fileId = fileData?.id; if (fileId) { const newFile: WorkflowFile = { id: fileId, fileId: fileId, fileName: fileData.fileName || file.name, fileSize: fileData.fileSize || file.size, mimeType: fileData.mimeType || file.type || 'application/octet-stream', source: 'user_uploaded' }; setPendingFiles(prev => { if (prev.some(f => f.fileId === fileId)) { return prev; } return [...prev, newFile]; }); } } return { success: result.success || false, data: result.fileData || null }; }, [workflowId, fileContext]); const handleFileAttach = useCallback(async (fileId: string): Promise => { const isInPending = pendingFiles.some(f => f.fileId === fileId); if (isInPending) { setPendingFiles(prev => prev.filter(f => f.fileId !== fileId)); } else { let workflowFile: WorkflowFile | null = null; const userFile = fileContext.files.find(f => f.id === fileId); if (userFile) { workflowFile = { id: userFile.id, fileId: userFile.id, fileName: userFile.file_name, fileSize: userFile.size || 0, mimeType: userFile.mime_type || 'application/octet-stream', source: 'user_uploaded' }; } else { const existingWorkflowFile = workflowFiles.find(f => f.fileId === fileId); if (existingWorkflowFile) { workflowFile = { ...existingWorkflowFile, id: existingWorkflowFile.id || existingWorkflowFile.fileId, fileId: existingWorkflowFile.fileId, fileName: existingWorkflowFile.fileName || 'Unknown File', fileSize: existingWorkflowFile.fileSize || 0, mimeType: existingWorkflowFile.mimeType || 'application/octet-stream', source: existingWorkflowFile.source || 'user_uploaded' }; } } if (workflowFile) { setPendingFiles(prev => { if (prev.some(f => f.fileId === fileId)) { return prev; } return [...prev, workflowFile!]; }); } } }, [pendingFiles, fileContext.files, workflowFiles]); const handleFileUploadAndAttach = useCallback(async (file: File): Promise<{ success: boolean; data: any }> => { return await handleFileUpload(file); }, [handleFileUpload]); const handleFileRemove = useCallback(async (file: WorkflowFile) => { setPendingFiles(prev => prev.filter(f => f.fileId !== file.fileId)); }, []); const handleFileDelete = useCallback(async (file: WorkflowFile) => { if (!file.fileId) return; const success = await fileContext.handleFileDelete(file.fileId, () => { setPendingFiles(prev => prev.filter(f => f.fileId !== file.fileId)); }); if (success) { setPendingFiles(prev => prev.filter(f => f.fileId !== file.fileId)); if (workflowId) { const messagesWithFile = messages.filter((msg: WorkflowMessage) => { const docs = (msg as any).documents as MessageDocument[] | undefined; return docs?.some(doc => doc.fileId === file.fileId); }); for (const message of messagesWithFile) { try { await deleteFileFromMessageApi(request, workflowId, message.id, file.fileId); } catch (error) { } } } } }, [workflowId, messages, fileContext, request]); const onInputChange = useCallback((value: string) => { setInputValue(value); }, []); const handleSubmit = useCallback(async () => { if (isRunning && workflowId) { try { const result = await stopWorkflow(); if (result.success) { resetWorkflow(); } } catch (error) { } return; } const trimmedInput = inputValue.trim(); if (!trimmedInput || startingWorkflow) { return; } try { const filesToSend = pendingFiles.filter(file => file.fileId); const fileIdsToSend = filesToSend.map(f => f.fileId).filter((id): id is string => !!id); const sentFileIdsSet = new Set(fileIdsToSend); // Optimistically render user message immediately const optimisticMsg: WorkflowMessage = { id: `optimistic-${Date.now()}`, workflowId: workflowId || '', message: trimmedInput, role: 'user', publishedAt: Date.now(), documents: filesToSend.map(file => ({ id: file.id || file.fileId, fileId: file.fileId, fileName: file.fileName, fileSize: file.fileSize, mimeType: file.mimeType, messageId: `optimistic-${Date.now()}`, roundNumber: 0, taskNumber: 0, actionNumber: 0, actionId: '' })) }; setOptimisticMessage(optimisticMsg); // Optimistically update workflow status to 'running' immediately if (setWorkflowStatusOptimistic) { setWorkflowStatusOptimistic('running'); } setPendingFiles(prev => prev.filter(file => !file.fileId || !sentFileIdsSet.has(file.fileId) )); if (!chatWorkflowPermission || chatWorkflowPermission.create === 'n') { setOptimisticMessage(null); if (setWorkflowStatusOptimistic) { setWorkflowStatusOptimistic('idle'); } return; } const selectedMode = workflowMode || 'Dynamic'; const apiWorkflowMode: 'Dynamic' | 'Automation' = selectedMode; const workflowOptions: { workflowId?: string; workflowMode: 'Dynamic' | 'Automation' } = { workflowMode: apiWorkflowMode }; if (workflowId) { workflowOptions.workflowId = workflowId; } const requestBody = { prompt: trimmedInput, listFileId: fileIdsToSend.length > 0 ? fileIdsToSend : undefined, userLanguage: 'en' }; const result = await startWorkflow(requestBody, workflowOptions); if (result.success) { setInputValue(''); const wasNewWorkflow = !workflowId; if (wasNewWorkflow && result.data) { const workflow = result.data as Workflow; // Dispatch event first to trigger refetch in useWorkflows window.dispatchEvent(new CustomEvent('workflowCreated', { detail: { workflow } })); // Refetch workflows list to ensure dropdown is updated await refetchWorkflows(); // Update context first (this will trigger the sync effect to update lifecycle) selectWorkflowFromContext(workflow.id); // Also directly update lifecycle to ensure immediate state update await selectWorkflow(workflow.id); } else if (workflowId) { // For resumed workflows, ensure context is synced and update lifecycle selectWorkflowFromContext(workflowId); await selectWorkflow(workflowId); } } else { setOptimisticMessage(null); if (setWorkflowStatusOptimistic) { setWorkflowStatusOptimistic('idle'); } } } catch (error) { setOptimisticMessage(null); if (setWorkflowStatusOptimistic) { setWorkflowStatusOptimistic('idle'); } } }, [inputValue, pendingFiles, isRunning, workflowId, startingWorkflow, startWorkflow, stopWorkflow, resetWorkflow, refetchWorkflows, selectWorkflowFromContext, selectWorkflow, chatWorkflowPermission, workflowMode, setWorkflowStatusOptimistic]); useEffect(() => { const handleWorkflowCleared = () => { // Reset all workflow-related state setPendingFiles([]); setOptimisticMessage(null); // Reset workflow lifecycle state resetWorkflow(); // Clear context selection clearWorkflowFromContext(); }; window.addEventListener('workflowCleared', handleWorkflowCleared); return () => { window.removeEventListener('workflowCleared', handleWorkflowCleared); }; }, [resetWorkflow, clearWorkflowFromContext]); const handleWorkflowSelect = useCallback(async (item: { id: string | number; label: string; value: any; metadata?: Record } | null) => { if (item === null) { clearWorkflowFromContext(); resetWorkflow(); setPendingFiles([]); setOptimisticMessage(null); return; } const workflowIdToSelect = typeof item.id === 'string' ? item.id : String(item.id); selectWorkflowFromContext(workflowIdToSelect); if (selectWorkflow) { await selectWorkflow(workflowIdToSelect); } }, [selectWorkflow, resetWorkflow, selectWorkflowFromContext, clearWorkflowFromContext]); const handlePromptSelect = useCallback(async (item: { id: string | number; label: string; value: any; metadata?: Record } | null) => { if (item === null) { setSelectedPromptId(null); return; } const promptId = typeof item.id === 'string' ? item.id : String(item.id); if (!promptPermission || promptPermission.read === 'n') { return; } try { const prompt = await fetchPromptById(promptId); if (prompt && prompt.content) { setSelectedPromptId(promptId); setInputValue(prompt.content); } } catch (error: any) { } }, [fetchPromptById, promptPermission]); const handleWorkflowModeSelect = useCallback((item: { id: string | number; label: string; value: any; metadata?: Record } | null) => { if (item === null) { setWorkflowMode(null); return; } const modeValue = item.value || item.id; const modeString = typeof modeValue === 'string' ? modeValue : String(modeValue); if (modeString === 'Dynamic' || modeString === 'Automation') { const mode = modeString as 'Dynamic' | 'Automation'; setWorkflowMode(mode); } }, []); const workflowItems = useMemo(() => { console.log('🔄 useDashboardInputForm: Computing workflowItems from workflows:', workflows); if (!workflows || !Array.isArray(workflows)) { console.warn('âš ī¸ useDashboardInputForm: workflows is not an array:', workflows); return []; } if (workflows.length === 0) { console.log('â„šī¸ useDashboardInputForm: workflows array is empty'); return []; } const items = workflows.map(workflow => ({ id: workflow.id, label: workflow.name || workflow.id, value: workflow, metadata: { status: workflow.status, workflowMode: workflow.workflowMode } })); console.log(`✅ useDashboardInputForm: Created ${items.length} workflow items:`, items); return items; }, [workflows]); const promptItems = useMemo(() => { if (!promptPermission || promptPermission.view === false || promptPermission.read === 'n') { return []; } return prompts.map(prompt => ({ id: prompt.id, label: prompt.name || prompt.id, value: prompt, metadata: { content: prompt.content } })); }, [prompts, promptPermission]); const workflowModeItems = useMemo(() => [ { id: 'Automation', label: 'Automation', value: 'Automation' as const, metadata: { description: 'Automated workflow processing' } }, { id: 'Dynamic', label: 'Dynamic', value: 'Dynamic' as const, metadata: { description: 'Iterative dynamic-style processing' } } ], []); return { data: [], loading: false, error: null, inputValue, onInputChange, handleSubmit, isSubmitting: startingWorkflow || isStopping, workflowId: workflowId || undefined, workflowStatus, currentRound, isRunning, messages: displayMessages || [], logs: unifiedContentLogs || [], // Unified content logs (without operationId) dashboardTree, // Dashboard log tree (logs with operationId) onToggleOperationExpanded: toggleOperationExpanded, getChildOperations, workflowItems, selectedWorkflowId: workflowId || selectedWorkflowId || null, onWorkflowSelect: handleWorkflowSelect, workflowsLoading, promptItems, selectedPromptId, onPromptSelect: handlePromptSelect, promptsLoading, promptPermission, workflowModeItems, selectedWorkflowMode: workflowMode, onWorkflowModeSelect: handleWorkflowModeSelect, playgroundUIPermission, chatWorkflowPermission, filePermission, workflowFiles, pendingFiles, handleFileUpload, handleFileDelete, handleFileRemove, uploadingFile: fileContext.uploadingFile, deletingFiles: fileContext.deletingFiles, previewingFiles: fileContext.previewingFiles, isFileAttachmentPopupOpen, setIsFileAttachmentPopupOpen, allUserFiles: fileContext.files || [], handleFileAttach, handleFileUploadAndAttach, latestStats }; } export function createDashboardHook() { return () => useDashboardInputForm(); }