frontend_nyla/src/hooks/useWorkflows.ts
2025-08-22 13:09:24 +02:00

508 lines
No EOL
15 KiB
TypeScript

import { useState, useEffect } from 'react';
import { useApiRequest } from './useApi';
// Import the centralized workflow interfaces
import type { Workflow, WorkflowMessage, WorkflowStats, WorkflowDocument, WorkflowLog } from '../components/Dashboard/DashboardChat/dashboardChatAreaTypes';
export type { Workflow, WorkflowMessage, WorkflowStats, WorkflowDocument, WorkflowLog };
export interface StartWorkflowRequest {
prompt: string;
listFileId: string[];
}
export interface StartWorkflowResponse {
id: string;
status: string;
message: string;
}
// Workflows list hook
export function useWorkflows() {
const [workflows, setWorkflows] = useState<Workflow[]>([]);
const { request, isLoading: loading, error } = useApiRequest<null, Workflow[]>();
const fetchWorkflows = async () => {
try {
const data = await request({
url: '/api/workflows/',
method: 'get'
});
setWorkflows(data);
} catch (error) {
// Error is already handled by useApiRequest
}
};
useEffect(() => {
fetchWorkflows();
}, []);
return { workflows, loading, error, refetch: fetchWorkflows };
}
// Workflow operations hook
export function useWorkflowOperations() {
const [startingWorkflow, setStartingWorkflow] = useState(false);
const [stoppingWorkflows, setStoppingWorkflows] = useState<Set<string>>(new Set());
const [deletingWorkflows, setDeletingWorkflows] = useState<Set<string>>(new Set());
const { request, isLoading } = useApiRequest();
const [startError, setStartError] = useState<string | null>(null);
const [stopError, setStopError] = useState<string | null>(null);
const [deleteError, setDeleteError] = useState<string | null>(null);
const startWorkflow = async (workflowData: StartWorkflowRequest, workflowId?: string) => {
setStartError(null);
setStartingWorkflow(true);
try {
const response = await request({
url: '/api/workflows/start',
method: 'post',
data: workflowData,
params: workflowId ? { workflowId } : undefined
}) as StartWorkflowResponse;
return { success: true, data: response };
} catch (error: any) {
setStartError(error.message);
return { success: false, error: error.message };
} finally {
setStartingWorkflow(false);
}
};
const stopWorkflow = async (workflowId: string) => {
setStopError(null);
setStoppingWorkflows(prev => new Set(prev).add(workflowId));
try {
await request({
url: `/api/workflows/${workflowId}/stop`,
method: 'post'
});
return true;
} catch (error: any) {
setStopError(error.message);
return false;
} finally {
setStoppingWorkflows(prev => {
const newSet = new Set(prev);
newSet.delete(workflowId);
return newSet;
});
}
};
const deleteWorkflow = async (workflowId: string) => {
setDeleteError(null);
setDeletingWorkflows(prev => new Set(prev).add(workflowId));
try {
await request({
url: `/api/workflows/${workflowId}`,
method: 'delete'
});
return true;
} catch (error: any) {
setDeleteError(error.message);
return false;
} finally {
setDeletingWorkflows(prev => {
const newSet = new Set(prev);
newSet.delete(workflowId);
return newSet;
});
}
};
const updateWorkflow = async (workflowId: string, updateData: Partial<{ name: string }>) => {
setDeleteError(null); // Reuse delete error state for update operations
try {
const updatedWorkflow = await request({
url: `/api/workflows/${workflowId}`,
method: 'put',
data: updateData
});
return { success: true, data: updatedWorkflow };
} catch (error: any) {
setDeleteError(error.message);
return { success: false, error: error.message };
}
};
return {
startingWorkflow,
stoppingWorkflows,
deletingWorkflows,
startError,
stopError,
deleteError,
startWorkflow,
stopWorkflow,
deleteWorkflow,
updateWorkflow,
isLoading
};
}
// Workflow status hook
export function useWorkflowStatus(workflowId: string | null) {
const [status, setStatus] = useState<Workflow | null>(null);
const { request, isLoading: loading, error } = useApiRequest<null, Workflow>();
const fetchStatus = async () => {
if (!workflowId) {
// Clear status when no workflow is selected
setStatus(null);
return;
}
try {
const data = await request({
url: `/api/workflows/${workflowId}/status`,
method: 'get'
});
setStatus(data);
} catch (error) {
// Error is already handled by useApiRequest
}
};
useEffect(() => {
fetchStatus();
}, [workflowId]);
return { status, loading, error, refetch: fetchStatus };
}
// Enhanced workflow messages hook with better typing
export function useWorkflowMessages(workflowId: string | null) {
const [messages, setMessages] = useState<WorkflowMessage[]>([]);
const [lastFetchTime, setLastFetchTime] = useState<number>(0);
const { request, isLoading: loading, error } = useApiRequest<null, WorkflowMessage[]>();
const fetchMessages = async () => {
if (!workflowId) {
setMessages([]);
return [];
}
try {
console.log(`📡 Fetching messages for workflow: ${workflowId}`);
const data = await request({
url: `/api/workflows/${workflowId}/messages`,
method: 'get'
});
console.log(`📨 Raw API response for messages:`, {
url: `/api/workflows/${workflowId}/messages`,
responseType: typeof data,
isArray: Array.isArray(data),
length: data?.length,
rawData: data,
firstMessage: data?.[0],
firstMessageKeys: data?.[0] ? Object.keys(data[0]) : []
});
// Only update if data has actually changed
const hasChanged = JSON.stringify(data) !== JSON.stringify(messages);
if (hasChanged) {
console.log(`📝 Messages updated: ${messages.length}${data?.length || 0}`);
setMessages(data);
setLastFetchTime(Date.now());
} else {
console.log(`📝 No changes in messages (${data?.length || 0} messages)`);
}
return data;
} catch (error) {
console.error('Failed to fetch workflow messages:', error);
return [];
}
};
useEffect(() => {
fetchMessages();
}, [workflowId]);
return { messages, loading, error, refetch: fetchMessages, lastFetchTime };
}
// Get single workflow hook
export function useWorkflow(workflowId: string | null) {
const [workflow, setWorkflow] = useState<Workflow | null>(null);
const { request, isLoading: loading, error } = useApiRequest<null, Workflow>();
const fetchWorkflow = async () => {
if (!workflowId) {
setWorkflow(null);
return null;
}
try {
const data = await request({
url: `/api/workflows/${workflowId}`,
method: 'get'
});
setWorkflow(data);
return data;
} catch (error) {
console.error('Failed to fetch workflow:', error);
return null;
}
};
useEffect(() => {
fetchWorkflow();
}, [workflowId]);
return { workflow, loading, error, refetch: fetchWorkflow };
}
// Enhanced workflow logs hook with better typing
export function useWorkflowLogs(workflowId: string | null) {
const [logs, setLogs] = useState<WorkflowLog[]>([]);
const [lastFetchTime, setLastFetchTime] = useState<number>(0);
const { request, isLoading: loading, error } = useApiRequest<null, WorkflowLog[]>();
const fetchLogs = async () => {
if (!workflowId) {
setLogs([]);
return [];
}
try {
console.log(`📡 Fetching logs for workflow: ${workflowId}`);
const data = await request({
url: `/api/workflows/${workflowId}/logs`,
method: 'get'
});
console.log(`📋 Raw API response for logs:`, {
url: `/api/workflows/${workflowId}/logs`,
responseType: typeof data,
isArray: Array.isArray(data),
length: data?.length,
rawData: data,
firstLog: data?.[0],
firstLogKeys: data?.[0] ? Object.keys(data[0]) : []
});
// Only update if data has actually changed
const hasChanged = JSON.stringify(data) !== JSON.stringify(logs);
if (hasChanged) {
console.log(`📋 Logs updated: ${logs.length}${data?.length || 0}`);
setLogs(data || []);
setLastFetchTime(Date.now());
} else {
console.log(`📋 No changes in logs (${data?.length || 0} logs)`);
}
return data || [];
} catch (error) {
console.error('Failed to fetch workflow logs:', error);
return [];
}
};
useEffect(() => {
fetchLogs();
}, [workflowId]);
return { logs, loading, error, refetch: fetchLogs, lastFetchTime };
}
// File preview hook
export function useFilePreview() {
const [previewContent, setPreviewContent] = useState<string | null>(null);
const [fileMetadata, setFileMetadata] = useState<any>(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const { request } = useApiRequest();
const fetchPreview = async (fileId: string | number) => {
if (!fileId) {
setError("File ID not available");
return;
}
setIsLoading(true);
setError(null);
setPreviewContent(null);
setFileMetadata(null);
try {
// Convert fileId to number since backend expects integer
let numericFileId: number;
if (typeof fileId === 'number') {
numericFileId = fileId;
} else {
numericFileId = parseInt(String(fileId), 10);
}
if (isNaN(numericFileId)) {
throw new Error(`Invalid file ID format: "${fileId}" (type: ${typeof fileId}). Expected a numeric file ID, but got a document UUID. Make sure the document object has a 'fileId' property with the numeric file ID.`);
}
const response = await request({
url: `/api/files/${numericFileId}/preview`,
method: 'get'
});
// Handle response as object with metadata and preview content
if (typeof response === 'object' && response !== null) {
setFileMetadata(response);
// Debug: log the full response
console.log('Full backend response:', response);
console.log('Response keys:', Object.keys(response));
// Try different possible property names for the content
const content = response.preview || response.content || response.data || response.previewContent;
console.log('Extracted content:', content ? 'has content' : 'null/empty');
console.log('Content type:', typeof content);
console.log('Content length:', content?.length);
// If base64Encoded is true and we have content, try to decode it
let processedContent = content;
if (response.base64Encoded && content && typeof content === 'string') {
try {
processedContent = atob(content);
console.log('Decoded base64 content:', processedContent.substring(0, 200));
} catch (e) {
console.error('Failed to decode base64 content:', e);
}
}
// If no preview content but file should be previewable, try to fetch raw content
if (!processedContent && response.name) {
const fileExtension = response.name.split('.').pop()?.toLowerCase();
const shouldBePreviewable = ['md', 'markdown', 'txt', 'py', 'js', 'ts', 'jsx', 'tsx', 'html', 'css', 'json', 'xml', 'yaml', 'yml'].includes(fileExtension || '');
if (shouldBePreviewable) {
console.log('File should be previewable, attempting to fetch raw content...');
try {
// Try to fetch the raw file content using download endpoint
const rawResponse = await request({
url: `/api/files/${numericFileId}/download`,
method: 'get',
additionalConfig: { responseType: 'text' }
});
console.log('Raw content fetched:', typeof rawResponse, rawResponse?.substring?.(0, 200));
setPreviewContent(rawResponse || null);
return;
} catch (rawError) {
console.error('Failed to fetch raw content:', rawError);
}
}
}
setPreviewContent(processedContent || null);
} else {
// Fallback if response is just the content
setPreviewContent(response);
}
} catch (err: any) {
setError(err.message || "Failed to load preview");
} finally {
setIsLoading(false);
}
};
const clearPreview = () => {
setPreviewContent(null);
setFileMetadata(null);
setError(null);
setIsLoading(false);
};
return {
previewContent,
fileMetadata,
isLoading,
error,
fetchPreview,
clearPreview
};
}
// File download hook
export function useFileDownload() {
const [isDownloading, setIsDownloading] = useState(false);
const [error, setError] = useState<string | null>(null);
const { request } = useApiRequest();
const downloadFile = async (fileId: string | number, fileName?: string) => {
if (!fileId) {
setError("File ID not available");
return;
}
setIsDownloading(true);
setError(null);
try {
// Convert fileId to number since backend expects integer
let numericFileId: number;
if (typeof fileId === 'number') {
numericFileId = fileId;
} else {
numericFileId = parseInt(String(fileId), 10);
}
if (isNaN(numericFileId)) {
throw new Error(`Invalid file ID format: "${fileId}" (type: ${typeof fileId}). Expected a numeric file ID, but got a document UUID. Make sure the document object has a 'fileId' property with the numeric file ID.`);
}
// Use the same approach as useFiles.ts - use request with blob response type
const blob = await request({
url: `/api/files/${numericFileId}/download`,
method: 'get',
// Override axios config for blob response
additionalConfig: { responseType: 'blob' }
});
// Use provided fileName or fallback to 'download'
const downloadFileName = fileName || 'download';
// Create download link and trigger download (same as useFiles.ts)
const url = window.URL.createObjectURL(new Blob([blob]));
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', downloadFileName);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
return true;
} catch (err: any) {
setError(err.message || "Failed to download file");
return false;
} finally {
setIsDownloading(false);
}
};
const clearError = () => {
setError(null);
};
return {
isDownloading,
error,
downloadFile,
clearError
};
}