workflow fixes

This commit is contained in:
ValueOn AG 2026-03-16 14:26:37 +01:00
parent 9e74daeaa5
commit d6d57a2113
8 changed files with 67 additions and 130 deletions

View file

@ -49,9 +49,12 @@ export const UserSection: React.FC = () => {
}
// Initialen für Avatar
const initials = user.fullName
? user.fullName.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2)
: user.username.slice(0, 2).toUpperCase();
const initials = (() => {
const name = user.fullName || user.username || '';
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 (
<div className={styles.userSection}>

View file

@ -2,90 +2,32 @@ import React, { useMemo } from 'react';
import { WorkflowStatusProps, WorkflowStatusType } from './WorkflowStatusTypes';
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 } => {
// First, check for completion messages with success status (these take priority)
const completionMessages = logs.filter(log => {
const message = (log.message || '').toLowerCase();
if (!logs.length) return { status: null, round: null, timestamp: 0 };
const sorted = [...logs].sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0));
for (const log of sorted) {
const logStatus = (log.status || '').toLowerCase();
return (message.includes('fast path completed') ||
message.includes('completed successfully')) &&
logStatus === 'success';
});
// 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);
}
}
const mapped = _STATUS_MAP[logStatus];
if (mapped) {
const roundMatch = (log.message || '').match(/\(?round\s+(\d+)\)?/i);
return { status: mapped, round: roundMatch ? parseInt(roundMatch[1], 10) : null, timestamp: log.timestamp || 0 };
}
return {
status: 'completed',
round,
timestamp: latestCompletion.timestamp || 0
};
}
// If no completion messages, look for workflow started/resumed/stopped messages
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
};
return { status: null, round: null, timestamp: 0 };
};
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
const workflowStatus = useMemo(() => {
// If we have status from API, use it
if (workflowStatusFromApi) {
let status: WorkflowStatusType = null;
const statusLower = workflowStatusFromApi.toLowerCase();
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
};
const mapped = _STATUS_MAP[workflowStatusFromApi.toLowerCase()] || null;
return { status: mapped, round: currentRoundFromApi || null, timestamp: Date.now() / 1000 };
}
// Fallback to extracting from logs
return extractWorkflowStatus(logs);
}, [workflowStatusFromApi, currentRoundFromApi, logs]);

View file

@ -276,12 +276,7 @@ export function useUserWorkflows(options?: { instanceId?: string; featureCode?:
} else if (attr.type === 'textarea') {
fieldType = 'textarea';
} else if (attr.type === 'text') {
// Check if it should be textarea based on name
if (attr.name.toLowerCase().includes('description') || attr.name.toLowerCase().includes('note')) {
fieldType = 'textarea';
} else {
fieldType = 'string';
}
fieldType = (attr as any).multiline === true ? 'textarea' : 'string';
}
// 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'

View file

@ -22,12 +22,16 @@ interface ConversationListProps {
instanceId: string;
activeWorkflowId: string | null;
onSelect: (workflowId: string) => void;
onCreateNew?: () => void;
refreshTrigger?: number;
}
export const ConversationList: React.FC<ConversationListProps> = ({
instanceId,
activeWorkflowId,
onSelect,
onCreateNew,
refreshTrigger,
}) => {
const [conversations, setConversations] = useState<Conversation[]>([]);
const [loading, setLoading] = useState(false);
@ -65,6 +69,16 @@ export const ConversationList: React.FC<ConversationListProps> = ({
_loadConversations();
}, [_loadConversations]);
useEffect(() => {
if (refreshTrigger) _loadConversations();
}, [refreshTrigger, _loadConversations]);
useEffect(() => {
if (activeWorkflowId && !conversations.find(c => c.id === activeWorkflowId)) {
_loadConversations();
}
}, [activeWorkflowId, conversations, _loadConversations]);
useEffect(() => {
if (editingId && inputRef.current) {
inputRef.current.focus();
@ -145,15 +159,7 @@ export const ConversationList: React.FC<ConversationListProps> = ({
};
const _handleCreateNew = () => {
api.post(`/api/workspace/${instanceId}/workflows`, {})
.then(res => {
const wf = res.data;
if (wf?.id) {
_loadConversations();
onSelect(wf.id);
}
})
.catch(() => {});
if (onCreateNew) onCreateNew();
};
const _filtered = (items: Conversation[], query: string): Conversation[] => {

View file

@ -108,7 +108,6 @@ export const WorkspaceInput: React.FC<WorkspaceInputProps> = ({
setShowAutocomplete(false);
setShowSourcePicker(false);
setAttachedFileIds([]);
setAttachedDataSourceIds([]);
}, [prompt, isProcessing, _extractFileRefs, attachedFileIds, attachedDataSourceIds, onSend]);
const _handleKeyDown = useCallback(

View file

@ -196,6 +196,8 @@ export const WorkspacePage: React.FC<WorkspacePageProps> = ({ persistentInstance
instanceId={instanceId}
activeWorkflowId={workspace.workflowId}
onSelect={_handleConversationSelect}
onCreateNew={workspace.resetToNew}
refreshTrigger={workspace.workflowVersion}
/>
)}
{leftTab === 'files' && (

View file

@ -77,6 +77,7 @@ interface UseWorkspaceReturn {
sendMessage: (prompt: string, fileIds?: string[], dataSourceIds?: string[], allowedProviders?: string[]) => void;
stopProcessing: () => void;
loadWorkflow: (workflowId: string) => void;
resetToNew: () => void;
files: WorkspaceFile[];
folders: WorkspaceFolder[];
dataSources: DataSource[];
@ -86,6 +87,7 @@ interface UseWorkspaceReturn {
acceptEdit: (editId: string) => void;
rejectEdit: (editId: string) => void;
workflowId: string | null;
workflowVersion: number;
refreshFiles: () => void;
refreshFolders: () => void;
refreshDataSources: () => void;
@ -102,6 +104,7 @@ export function useWorkspace(instanceId: string): UseWorkspaceReturn {
const [toolActivities, setToolActivities] = useState<ToolActivity[]>([]);
const [pendingEdits, setPendingEdits] = useState<FileEditProposal[]>([]);
const [workflowId, setWorkflowId] = useState<string | null>(null);
const [workflowVersion, setWorkflowVersion] = useState(0);
const [dataSourceAccesses, setDataSourceAccesses] = useState<DataSourceAccessEvent[]>([]);
const cleanupRef = useRef<(() => void) | null>(null);
@ -156,6 +159,15 @@ export function useWorkspace(instanceId: string): UseWorkspaceReturn {
.catch(() => {});
}, [instanceId]);
const resetToNew = useCallback(() => {
setWorkflowId(null);
setMessages([]);
setToolActivities([]);
setPendingEdits([]);
setAgentProgress(null);
setDataSourceAccesses([]);
}, []);
const sendMessage = useCallback(
(prompt: string, fileIds: string[] = [], dataSourceIds: string[] = [], allowedProviders: string[] = []) => {
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) => {
setIsProcessing(false);
if (event.workflowId) setWorkflowId(event.workflowId);
@ -365,6 +381,7 @@ export function useWorkspace(instanceId: string): UseWorkspaceReturn {
sendMessage,
stopProcessing,
loadWorkflow,
resetToNew,
files,
folders,
dataSources,
@ -374,6 +391,7 @@ export function useWorkspace(instanceId: string): UseWorkspaceReturn {
acceptEdit,
rejectEdit,
workflowId,
workflowVersion,
refreshFiles,
refreshFolders,
refreshDataSources,

View file

@ -26,6 +26,7 @@ export interface SseEventHandlers {
onFileCreated?: (event: SseEvent) => void;
onDataSourceAccess?: (event: SseEvent) => void;
onVoiceResponse?: (event: SseEvent) => void;
onWorkflowUpdated?: (event: SseEvent) => void;
onComplete?: (event: SseEvent) => void;
onStopped?: (event: SseEvent) => void;
onError?: (event: SseEvent) => void;
@ -58,6 +59,7 @@ const _EVENT_ROUTER: Record<string, keyof SseEventHandlers> = {
fileCreated: 'onFileCreated',
dataSourceAccess: 'onDataSourceAccess',
voiceResponse: 'onVoiceResponse',
workflowUpdated: 'onWorkflowUpdated',
complete: 'onComplete',
stopped: 'onStopped',
error: 'onError',