508 lines
No EOL
15 KiB
TypeScript
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
|
|
};
|
|
}
|