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 { 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(instanceId: string) { 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 [selectedProviders, setSelectedProviders] = useState([]); // AI provider selection (multiselect) 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(instanceId); // Dashboard log tree hook const { tree: dashboardTree, processDashboardLogs, clearDashboard, toggleOperationExpanded, toggleRoundExpanded, 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', 'ui.system.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; if (!optimisticMessage) return; // Clear optimistic message when backend's "first" user message arrives via polling. // The backend message contains the normalizedRequest (which differs from the original prompt), // so we match by status="first" instead of content comparison. const hasFirstMessage = messages.some((msg: WorkflowMessage) => (msg as any).status === 'first' && msg.role?.toLowerCase() === 'user' ); if (hasFirstMessage) { setOptimisticMessage(null); } }, [messages, optimisticMessage]); const displayMessages = useMemo(() => { 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; }); // If optimistic message is still active (backend "first" message not yet polled), // show the optimistic message instead of any backend user messages to avoid duplicates. const allMessages = [...processedMessages]; if (optimisticMessage) { // Find backend "first" user message to inherit its timestamp for correct ordering const firstBackendMsg = processedMessages.find((msg: WorkflowMessage) => (msg as any).status === 'first' && msg.role?.toLowerCase() === 'user' ); if (!firstBackendMsg) { // Backend "first" message not yet arrived - show optimistic message allMessages.push(optimisticMessage); } // If firstBackendMsg exists, the useEffect above will clear optimistic on next render } 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]); // handleFileView is a no-op because ViewActionButton's ContentPreview handles the preview internally const handleFileView = useCallback(async (_file: WorkflowFile) => { // The ViewActionButton component handles the preview via ContentPreview // No additional action needed here }, []); const handleFileDownload = useCallback(async (file: WorkflowFile) => { if (!file.fileId) return; await fileContext.handleFileDownload(file.fileId, file.fileName); }, [fileContext]); const onInputChange = useCallback((value: string) => { setInputValue(value); }, []); // Separate stop handler - only stops the workflow without sending new input const handleStop = useCallback(async () => { if (!workflowId) return { success: false, error: 'No workflow to stop' }; try { const result = await stopWorkflow(); return result; } catch (error: any) { return { success: false, error: error.message || 'Failed to stop workflow' }; } }, [workflowId, stopWorkflow]); const handleSubmit = useCallback(async () => { const trimmedInput = inputValue.trim(); // If running and no new input, just stop if (isRunning && workflowId && !trimmedInput) { try { await stopWorkflow(); } catch (error) { // Ignore stop errors } return; } // If running with new input, stop first then continue with new input if (isRunning && workflowId && trimmedInput) { try { // Stop the current workflow await stopWorkflow(); // Continue below to send new input } catch (error) { // Ignore stop errors, try to continue anyway } } // No input and not running = nothing to do if (!trimmedInput || startingWorkflow) { return; } 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', allowedProviders: selectedProviders.length > 0 ? selectedProviders : undefined // AI provider filter (multiselect) }; // Debug: Log provider selection console.log('🤖 Provider selection:', { selectedProviders, sentProviders: requestBody.allowedProviders }); 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, selectedProviders, 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, handleStop, isSubmitting: startingWorkflow || isStopping, 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, onToggleRoundExpanded: toggleRoundExpanded, 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, handleFileView, uploadingFile: fileContext.uploadingFile, deletingFiles: fileContext.deletingFiles, previewingFiles: fileContext.previewingFiles, downloadingFiles: fileContext.downloadingFiles, handleFileDownload, isFileAttachmentPopupOpen, setIsFileAttachmentPopupOpen, allUserFiles: fileContext.files || [], handleFileAttach, handleFileUploadAndAttach, latestStats, // AI Provider selection (multiselect) selectedProviders, onProvidersChange: setSelectedProviders }; } export function createDashboardHook(instanceId: string) { return () => useDashboardInputForm(instanceId); }