workflow fixes
This commit is contained in:
parent
9e74daeaa5
commit
d6d57a2113
8 changed files with 67 additions and 130 deletions
|
|
@ -49,9 +49,12 @@ export const UserSection: React.FC = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialen für Avatar
|
// Initialen für Avatar
|
||||||
const initials = user.fullName
|
const initials = (() => {
|
||||||
? user.fullName.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2)
|
const name = user.fullName || user.username || '';
|
||||||
: user.username.slice(0, 2).toUpperCase();
|
const parts = name.trim().split(/\s+/);
|
||||||
|
if (parts.length >= 2) return (parts[0][0] + parts[parts.length - 1][0]).toLocaleUpperCase();
|
||||||
|
return [...name.trim()].slice(0, 2).join('').toLocaleUpperCase() || '?';
|
||||||
|
})();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.userSection}>
|
<div className={styles.userSection}>
|
||||||
|
|
|
||||||
|
|
@ -2,90 +2,32 @@ import React, { useMemo } from 'react';
|
||||||
import { WorkflowStatusProps, WorkflowStatusType } from './WorkflowStatusTypes';
|
import { WorkflowStatusProps, WorkflowStatusType } from './WorkflowStatusTypes';
|
||||||
import styles from './WorkflowStatus.module.css';
|
import styles from './WorkflowStatus.module.css';
|
||||||
|
|
||||||
// Helper function to extract workflow status and round from log message
|
const _STATUS_MAP: Record<string, WorkflowStatusType> = {
|
||||||
|
success: 'completed',
|
||||||
|
completed: 'completed',
|
||||||
|
started: 'started',
|
||||||
|
running: 'started',
|
||||||
|
resumed: 'resumed',
|
||||||
|
stopped: 'stopped',
|
||||||
|
failed: 'failed',
|
||||||
|
error: 'failed',
|
||||||
|
};
|
||||||
|
|
||||||
const extractWorkflowStatus = (logs: any[]): { status: WorkflowStatusType; round: number | null; timestamp: number } => {
|
const extractWorkflowStatus = (logs: any[]): { status: WorkflowStatusType; round: number | null; timestamp: number } => {
|
||||||
// First, check for completion messages with success status (these take priority)
|
if (!logs.length) return { status: null, round: null, timestamp: 0 };
|
||||||
const completionMessages = logs.filter(log => {
|
|
||||||
const message = (log.message || '').toLowerCase();
|
const sorted = [...logs].sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0));
|
||||||
|
|
||||||
|
for (const log of sorted) {
|
||||||
const logStatus = (log.status || '').toLowerCase();
|
const logStatus = (log.status || '').toLowerCase();
|
||||||
return (message.includes('fast path completed') ||
|
const mapped = _STATUS_MAP[logStatus];
|
||||||
message.includes('completed successfully')) &&
|
if (mapped) {
|
||||||
logStatus === 'success';
|
const roundMatch = (log.message || '').match(/\(?round\s+(\d+)\)?/i);
|
||||||
});
|
return { status: mapped, round: roundMatch ? parseInt(roundMatch[1], 10) : null, timestamp: log.timestamp || 0 };
|
||||||
|
|
||||||
// If we have completion messages, use the latest one
|
|
||||||
if (completionMessages.length > 0) {
|
|
||||||
const latestCompletion = completionMessages.sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0))[0];
|
|
||||||
|
|
||||||
// Try to extract round from completion message
|
|
||||||
let round: number | null = null;
|
|
||||||
const message = (latestCompletion.message || '').toLowerCase();
|
|
||||||
const roundMatch = message.match(/\(?round\s+(\d+)\)?/i);
|
|
||||||
if (roundMatch) {
|
|
||||||
round = parseInt(roundMatch[1], 10);
|
|
||||||
} else {
|
|
||||||
// If no round in completion message, get round from latest workflow status message
|
|
||||||
const statusMessages = logs.filter(log => {
|
|
||||||
const msg = (log.message || '').toLowerCase();
|
|
||||||
return msg.includes('workflow started') || msg.includes('workflow resumed');
|
|
||||||
});
|
|
||||||
if (statusMessages.length > 0) {
|
|
||||||
const latestWorkflowStatus = statusMessages.sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0))[0];
|
|
||||||
const workflowMessage = (latestWorkflowStatus.message || '').toLowerCase();
|
|
||||||
const workflowRoundMatch = workflowMessage.match(/\(?round\s+(\d+)\)?/i);
|
|
||||||
if (workflowRoundMatch) {
|
|
||||||
round = parseInt(workflowRoundMatch[1], 10);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
|
||||||
status: 'completed',
|
|
||||||
round,
|
|
||||||
timestamp: latestCompletion.timestamp || 0
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no completion messages, look for workflow started/resumed/stopped messages
|
return { status: null, round: null, timestamp: 0 };
|
||||||
const statusMessages = logs.filter(log => {
|
|
||||||
const message = (log.message || '').toLowerCase();
|
|
||||||
return message.includes('workflow started') ||
|
|
||||||
message.includes('workflow resumed') ||
|
|
||||||
message.includes('workflow stopped') ||
|
|
||||||
message.includes('workflow failed') ||
|
|
||||||
message.includes('workflow completed');
|
|
||||||
});
|
|
||||||
|
|
||||||
if (statusMessages.length === 0) {
|
|
||||||
return { status: null, round: null, timestamp: 0 };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the latest status message
|
|
||||||
const latestStatus = statusMessages.sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0))[0];
|
|
||||||
const message = (latestStatus.message || '').toLowerCase();
|
|
||||||
|
|
||||||
let status: WorkflowStatusType = null;
|
|
||||||
if (message.includes('started')) {
|
|
||||||
status = 'started';
|
|
||||||
} else if (message.includes('resumed')) {
|
|
||||||
status = 'resumed';
|
|
||||||
} else if (message.includes('stopped')) {
|
|
||||||
status = 'stopped';
|
|
||||||
} else if (message.includes('failed')) {
|
|
||||||
status = 'failed';
|
|
||||||
} else if (message.includes('completed')) {
|
|
||||||
status = 'completed';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract round number from message (e.g., "round 4", "round 2", or "(round 4)")
|
|
||||||
const roundMatch = message.match(/\(?round\s+(\d+)\)?/i);
|
|
||||||
const round = roundMatch ? parseInt(roundMatch[1], 10) : null;
|
|
||||||
|
|
||||||
return {
|
|
||||||
status,
|
|
||||||
round,
|
|
||||||
timestamp: latestStatus.timestamp || 0
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const _formatCurrency = (amount?: number): string => {
|
const _formatCurrency = (amount?: number): string => {
|
||||||
|
|
@ -103,40 +45,10 @@ const WorkflowStatus: React.FC<WorkflowStatusProps> = ({
|
||||||
}) => {
|
}) => {
|
||||||
// Use workflow status and round from API response, fallback to extracting from logs
|
// Use workflow status and round from API response, fallback to extracting from logs
|
||||||
const workflowStatus = useMemo(() => {
|
const workflowStatus = useMemo(() => {
|
||||||
// If we have status from API, use it
|
|
||||||
if (workflowStatusFromApi) {
|
if (workflowStatusFromApi) {
|
||||||
let status: WorkflowStatusType = null;
|
const mapped = _STATUS_MAP[workflowStatusFromApi.toLowerCase()] || null;
|
||||||
const statusLower = workflowStatusFromApi.toLowerCase();
|
return { status: mapped, round: currentRoundFromApi || null, timestamp: Date.now() / 1000 };
|
||||||
|
|
||||||
if (statusLower === 'completed') {
|
|
||||||
status = 'completed';
|
|
||||||
} else if (statusLower === 'running') {
|
|
||||||
// Check if it's started or resumed from logs
|
|
||||||
const startedResumedLogs = logs.filter(log => {
|
|
||||||
const message = (log.message || '').toLowerCase();
|
|
||||||
return message.includes('workflow started') || message.includes('workflow resumed');
|
|
||||||
});
|
|
||||||
if (startedResumedLogs.length > 0) {
|
|
||||||
const latest = startedResumedLogs.sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0))[0];
|
|
||||||
const message = (latest.message || '').toLowerCase();
|
|
||||||
status = message.includes('resumed') ? 'resumed' : 'started';
|
|
||||||
} else {
|
|
||||||
status = 'started';
|
|
||||||
}
|
|
||||||
} else if (statusLower === 'stopped') {
|
|
||||||
status = 'stopped';
|
|
||||||
} else if (statusLower === 'failed') {
|
|
||||||
status = 'failed';
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
status,
|
|
||||||
round: currentRoundFromApi || null,
|
|
||||||
timestamp: Date.now() / 1000 // Use current time since we don't have timestamp from API
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to extracting from logs
|
|
||||||
return extractWorkflowStatus(logs);
|
return extractWorkflowStatus(logs);
|
||||||
}, [workflowStatusFromApi, currentRoundFromApi, logs]);
|
}, [workflowStatusFromApi, currentRoundFromApi, logs]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -276,12 +276,7 @@ export function useUserWorkflows(options?: { instanceId?: string; featureCode?:
|
||||||
} else if (attr.type === 'textarea') {
|
} else if (attr.type === 'textarea') {
|
||||||
fieldType = 'textarea';
|
fieldType = 'textarea';
|
||||||
} else if (attr.type === 'text') {
|
} else if (attr.type === 'text') {
|
||||||
// Check if it should be textarea based on name
|
fieldType = (attr as any).multiline === true ? 'textarea' : 'string';
|
||||||
if (attr.name.toLowerCase().includes('description') || attr.name.toLowerCase().includes('note')) {
|
|
||||||
fieldType = 'textarea';
|
|
||||||
} else {
|
|
||||||
fieldType = 'string';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// Note: Legacy 'boolean' and 'enum' types are not in the AttributeDefinition type union
|
// Note: Legacy 'boolean' and 'enum' types are not in the AttributeDefinition type union
|
||||||
// If needed, they should be handled via type casting: (attr as any).type === 'boolean'
|
// If needed, they should be handled via type casting: (attr as any).type === 'boolean'
|
||||||
|
|
|
||||||
|
|
@ -22,12 +22,16 @@ interface ConversationListProps {
|
||||||
instanceId: string;
|
instanceId: string;
|
||||||
activeWorkflowId: string | null;
|
activeWorkflowId: string | null;
|
||||||
onSelect: (workflowId: string) => void;
|
onSelect: (workflowId: string) => void;
|
||||||
|
onCreateNew?: () => void;
|
||||||
|
refreshTrigger?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ConversationList: React.FC<ConversationListProps> = ({
|
export const ConversationList: React.FC<ConversationListProps> = ({
|
||||||
instanceId,
|
instanceId,
|
||||||
activeWorkflowId,
|
activeWorkflowId,
|
||||||
onSelect,
|
onSelect,
|
||||||
|
onCreateNew,
|
||||||
|
refreshTrigger,
|
||||||
}) => {
|
}) => {
|
||||||
const [conversations, setConversations] = useState<Conversation[]>([]);
|
const [conversations, setConversations] = useState<Conversation[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
@ -65,6 +69,16 @@ export const ConversationList: React.FC<ConversationListProps> = ({
|
||||||
_loadConversations();
|
_loadConversations();
|
||||||
}, [_loadConversations]);
|
}, [_loadConversations]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (refreshTrigger) _loadConversations();
|
||||||
|
}, [refreshTrigger, _loadConversations]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (activeWorkflowId && !conversations.find(c => c.id === activeWorkflowId)) {
|
||||||
|
_loadConversations();
|
||||||
|
}
|
||||||
|
}, [activeWorkflowId, conversations, _loadConversations]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (editingId && inputRef.current) {
|
if (editingId && inputRef.current) {
|
||||||
inputRef.current.focus();
|
inputRef.current.focus();
|
||||||
|
|
@ -145,15 +159,7 @@ export const ConversationList: React.FC<ConversationListProps> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
const _handleCreateNew = () => {
|
const _handleCreateNew = () => {
|
||||||
api.post(`/api/workspace/${instanceId}/workflows`, {})
|
if (onCreateNew) onCreateNew();
|
||||||
.then(res => {
|
|
||||||
const wf = res.data;
|
|
||||||
if (wf?.id) {
|
|
||||||
_loadConversations();
|
|
||||||
onSelect(wf.id);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const _filtered = (items: Conversation[], query: string): Conversation[] => {
|
const _filtered = (items: Conversation[], query: string): Conversation[] => {
|
||||||
|
|
|
||||||
|
|
@ -108,7 +108,6 @@ export const WorkspaceInput: React.FC<WorkspaceInputProps> = ({
|
||||||
setShowAutocomplete(false);
|
setShowAutocomplete(false);
|
||||||
setShowSourcePicker(false);
|
setShowSourcePicker(false);
|
||||||
setAttachedFileIds([]);
|
setAttachedFileIds([]);
|
||||||
setAttachedDataSourceIds([]);
|
|
||||||
}, [prompt, isProcessing, _extractFileRefs, attachedFileIds, attachedDataSourceIds, onSend]);
|
}, [prompt, isProcessing, _extractFileRefs, attachedFileIds, attachedDataSourceIds, onSend]);
|
||||||
|
|
||||||
const _handleKeyDown = useCallback(
|
const _handleKeyDown = useCallback(
|
||||||
|
|
|
||||||
|
|
@ -196,6 +196,8 @@ export const WorkspacePage: React.FC<WorkspacePageProps> = ({ persistentInstance
|
||||||
instanceId={instanceId}
|
instanceId={instanceId}
|
||||||
activeWorkflowId={workspace.workflowId}
|
activeWorkflowId={workspace.workflowId}
|
||||||
onSelect={_handleConversationSelect}
|
onSelect={_handleConversationSelect}
|
||||||
|
onCreateNew={workspace.resetToNew}
|
||||||
|
refreshTrigger={workspace.workflowVersion}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{leftTab === 'files' && (
|
{leftTab === 'files' && (
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,7 @@ interface UseWorkspaceReturn {
|
||||||
sendMessage: (prompt: string, fileIds?: string[], dataSourceIds?: string[], allowedProviders?: string[]) => void;
|
sendMessage: (prompt: string, fileIds?: string[], dataSourceIds?: string[], allowedProviders?: string[]) => void;
|
||||||
stopProcessing: () => void;
|
stopProcessing: () => void;
|
||||||
loadWorkflow: (workflowId: string) => void;
|
loadWorkflow: (workflowId: string) => void;
|
||||||
|
resetToNew: () => void;
|
||||||
files: WorkspaceFile[];
|
files: WorkspaceFile[];
|
||||||
folders: WorkspaceFolder[];
|
folders: WorkspaceFolder[];
|
||||||
dataSources: DataSource[];
|
dataSources: DataSource[];
|
||||||
|
|
@ -86,6 +87,7 @@ interface UseWorkspaceReturn {
|
||||||
acceptEdit: (editId: string) => void;
|
acceptEdit: (editId: string) => void;
|
||||||
rejectEdit: (editId: string) => void;
|
rejectEdit: (editId: string) => void;
|
||||||
workflowId: string | null;
|
workflowId: string | null;
|
||||||
|
workflowVersion: number;
|
||||||
refreshFiles: () => void;
|
refreshFiles: () => void;
|
||||||
refreshFolders: () => void;
|
refreshFolders: () => void;
|
||||||
refreshDataSources: () => void;
|
refreshDataSources: () => void;
|
||||||
|
|
@ -102,6 +104,7 @@ export function useWorkspace(instanceId: string): UseWorkspaceReturn {
|
||||||
const [toolActivities, setToolActivities] = useState<ToolActivity[]>([]);
|
const [toolActivities, setToolActivities] = useState<ToolActivity[]>([]);
|
||||||
const [pendingEdits, setPendingEdits] = useState<FileEditProposal[]>([]);
|
const [pendingEdits, setPendingEdits] = useState<FileEditProposal[]>([]);
|
||||||
const [workflowId, setWorkflowId] = useState<string | null>(null);
|
const [workflowId, setWorkflowId] = useState<string | null>(null);
|
||||||
|
const [workflowVersion, setWorkflowVersion] = useState(0);
|
||||||
const [dataSourceAccesses, setDataSourceAccesses] = useState<DataSourceAccessEvent[]>([]);
|
const [dataSourceAccesses, setDataSourceAccesses] = useState<DataSourceAccessEvent[]>([]);
|
||||||
const cleanupRef = useRef<(() => void) | null>(null);
|
const cleanupRef = useRef<(() => void) | null>(null);
|
||||||
|
|
||||||
|
|
@ -156,6 +159,15 @@ export function useWorkspace(instanceId: string): UseWorkspaceReturn {
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
}, [instanceId]);
|
}, [instanceId]);
|
||||||
|
|
||||||
|
const resetToNew = useCallback(() => {
|
||||||
|
setWorkflowId(null);
|
||||||
|
setMessages([]);
|
||||||
|
setToolActivities([]);
|
||||||
|
setPendingEdits([]);
|
||||||
|
setAgentProgress(null);
|
||||||
|
setDataSourceAccesses([]);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const sendMessage = useCallback(
|
const sendMessage = useCallback(
|
||||||
(prompt: string, fileIds: string[] = [], dataSourceIds: string[] = [], allowedProviders: string[] = []) => {
|
(prompt: string, fileIds: string[] = [], dataSourceIds: string[] = [], allowedProviders: string[] = []) => {
|
||||||
if (!instanceId || isProcessing) return;
|
if (!instanceId || isProcessing) return;
|
||||||
|
|
@ -292,6 +304,10 @@ export function useWorkspace(instanceId: string): UseWorkspaceReturn {
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onWorkflowUpdated: (event) => {
|
||||||
|
if (event.workflowId) setWorkflowId(event.workflowId);
|
||||||
|
setWorkflowVersion(v => v + 1);
|
||||||
|
},
|
||||||
onComplete: (event) => {
|
onComplete: (event) => {
|
||||||
setIsProcessing(false);
|
setIsProcessing(false);
|
||||||
if (event.workflowId) setWorkflowId(event.workflowId);
|
if (event.workflowId) setWorkflowId(event.workflowId);
|
||||||
|
|
@ -365,6 +381,7 @@ export function useWorkspace(instanceId: string): UseWorkspaceReturn {
|
||||||
sendMessage,
|
sendMessage,
|
||||||
stopProcessing,
|
stopProcessing,
|
||||||
loadWorkflow,
|
loadWorkflow,
|
||||||
|
resetToNew,
|
||||||
files,
|
files,
|
||||||
folders,
|
folders,
|
||||||
dataSources,
|
dataSources,
|
||||||
|
|
@ -374,6 +391,7 @@ export function useWorkspace(instanceId: string): UseWorkspaceReturn {
|
||||||
acceptEdit,
|
acceptEdit,
|
||||||
rejectEdit,
|
rejectEdit,
|
||||||
workflowId,
|
workflowId,
|
||||||
|
workflowVersion,
|
||||||
refreshFiles,
|
refreshFiles,
|
||||||
refreshFolders,
|
refreshFolders,
|
||||||
refreshDataSources,
|
refreshDataSources,
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ export interface SseEventHandlers {
|
||||||
onFileCreated?: (event: SseEvent) => void;
|
onFileCreated?: (event: SseEvent) => void;
|
||||||
onDataSourceAccess?: (event: SseEvent) => void;
|
onDataSourceAccess?: (event: SseEvent) => void;
|
||||||
onVoiceResponse?: (event: SseEvent) => void;
|
onVoiceResponse?: (event: SseEvent) => void;
|
||||||
|
onWorkflowUpdated?: (event: SseEvent) => void;
|
||||||
onComplete?: (event: SseEvent) => void;
|
onComplete?: (event: SseEvent) => void;
|
||||||
onStopped?: (event: SseEvent) => void;
|
onStopped?: (event: SseEvent) => void;
|
||||||
onError?: (event: SseEvent) => void;
|
onError?: (event: SseEvent) => void;
|
||||||
|
|
@ -58,6 +59,7 @@ const _EVENT_ROUTER: Record<string, keyof SseEventHandlers> = {
|
||||||
fileCreated: 'onFileCreated',
|
fileCreated: 'onFileCreated',
|
||||||
dataSourceAccess: 'onDataSourceAccess',
|
dataSourceAccess: 'onDataSourceAccess',
|
||||||
voiceResponse: 'onVoiceResponse',
|
voiceResponse: 'onVoiceResponse',
|
||||||
|
workflowUpdated: 'onWorkflowUpdated',
|
||||||
complete: 'onComplete',
|
complete: 'onComplete',
|
||||||
stopped: 'onStopped',
|
stopped: 'onStopped',
|
||||||
error: 'onError',
|
error: 'onError',
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue