798 lines
28 KiB
TypeScript
798 lines
28 KiB
TypeScript
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<string>('');
|
||
const [pendingFiles, setPendingFiles] = useState<WorkflowFile[]>([]);
|
||
const [isFileAttachmentPopupOpen, setIsFileAttachmentPopupOpen] = useState(false);
|
||
const [optimisticMessage, setOptimisticMessage] = useState<WorkflowMessage | null>(null);
|
||
const [selectedPromptId, setSelectedPromptId] = useState<string | null>(null);
|
||
const [workflowMode, setWorkflowMode] = useState<'Dynamic' | 'Automation' | null>(null);
|
||
const [selectedProviders, setSelectedProviders] = useState<string[]>([]); // AI provider selection (multiselect)
|
||
|
||
const { checkPermission } = usePermissions();
|
||
const [playgroundUIPermission, setPlaygroundUIPermission] = useState<boolean>(true);
|
||
const [chatWorkflowPermission, setChatWorkflowPermission] = useState<any>(null);
|
||
const [promptPermission, setPromptPermission] = useState<any>(null);
|
||
const [filePermission, setFilePermission] = useState<any>(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 {
|
||
// UI permission is already verified by the navigation/routing layer
|
||
// (FeatureAccess + instance role checked before page is reachable).
|
||
// We set it to true and load DATA permissions directly.
|
||
setPlaygroundUIPermission(true);
|
||
|
||
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();
|
||
}, [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<Set<string>>(new Set());
|
||
const lastWorkflowIdRef = useRef<string | null>(null);
|
||
const lastDashboardLogsLengthRef = useRef<number>(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<string, WorkflowFile>();
|
||
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<void> => {
|
||
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();
|
||
// NOTE: Do NOT call clearWorkflowFromContext() here — this handler is
|
||
// triggered BY clearWorkflow() which already set the context to null.
|
||
// Calling it again would dispatch another 'workflowCleared' event → infinite recursion.
|
||
};
|
||
|
||
window.addEventListener('workflowCleared', handleWorkflowCleared);
|
||
return () => {
|
||
window.removeEventListener('workflowCleared', handleWorkflowCleared);
|
||
};
|
||
}, [resetWorkflow]);
|
||
|
||
const handleWorkflowSelect = useCallback(async (item: { id: string | number; label: string; value: any; metadata?: Record<string, any> } | 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<string, any> } | 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<string, any> } | 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);
|
||
}
|
||
|