frontend_nyla/src/hooks/playground/useDashboardInputForm.ts
2026-02-12 00:34:25 +01:00

798 lines
28 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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);
}