Date: Mon, 16 Mar 2026 14:26:37 +0100
Subject: [PATCH 8/8] workflow fixes
---
src/components/Navigation/UserSection.tsx | 9 +-
.../WorkflowStatus/WorkflowStatus.tsx | 134 +++---------------
src/hooks/useWorkflows.ts | 7 +-
.../views/workspace/ConversationList.tsx | 24 ++--
src/pages/views/workspace/WorkspaceInput.tsx | 1 -
src/pages/views/workspace/WorkspacePage.tsx | 2 +
src/pages/views/workspace/useWorkspace.ts | 18 +++
src/utils/sseClient.ts | 2 +
8 files changed, 67 insertions(+), 130 deletions(-)
diff --git a/src/components/Navigation/UserSection.tsx b/src/components/Navigation/UserSection.tsx
index e19cd0e..1b6f9e7 100644
--- a/src/components/Navigation/UserSection.tsx
+++ b/src/components/Navigation/UserSection.tsx
@@ -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 (
diff --git a/src/components/UiComponents/WorkflowStatus/WorkflowStatus.tsx b/src/components/UiComponents/WorkflowStatus/WorkflowStatus.tsx
index 3002c35..41d9692 100644
--- a/src/components/UiComponents/WorkflowStatus/WorkflowStatus.tsx
+++ b/src/components/UiComponents/WorkflowStatus/WorkflowStatus.tsx
@@ -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 = {
+ 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 = ({
}) => {
// 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]);
diff --git a/src/hooks/useWorkflows.ts b/src/hooks/useWorkflows.ts
index cdc25dc..ebaf1c5 100644
--- a/src/hooks/useWorkflows.ts
+++ b/src/hooks/useWorkflows.ts
@@ -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'
diff --git a/src/pages/views/workspace/ConversationList.tsx b/src/pages/views/workspace/ConversationList.tsx
index 7f59cbd..46abf22 100644
--- a/src/pages/views/workspace/ConversationList.tsx
+++ b/src/pages/views/workspace/ConversationList.tsx
@@ -22,12 +22,16 @@ interface ConversationListProps {
instanceId: string;
activeWorkflowId: string | null;
onSelect: (workflowId: string) => void;
+ onCreateNew?: () => void;
+ refreshTrigger?: number;
}
export const ConversationList: React.FC = ({
instanceId,
activeWorkflowId,
onSelect,
+ onCreateNew,
+ refreshTrigger,
}) => {
const [conversations, setConversations] = useState([]);
const [loading, setLoading] = useState(false);
@@ -65,6 +69,16 @@ export const ConversationList: React.FC = ({
_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 = ({
};
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[] => {
diff --git a/src/pages/views/workspace/WorkspaceInput.tsx b/src/pages/views/workspace/WorkspaceInput.tsx
index 5088b23..6ff1095 100644
--- a/src/pages/views/workspace/WorkspaceInput.tsx
+++ b/src/pages/views/workspace/WorkspaceInput.tsx
@@ -108,7 +108,6 @@ export const WorkspaceInput: React.FC = ({
setShowAutocomplete(false);
setShowSourcePicker(false);
setAttachedFileIds([]);
- setAttachedDataSourceIds([]);
}, [prompt, isProcessing, _extractFileRefs, attachedFileIds, attachedDataSourceIds, onSend]);
const _handleKeyDown = useCallback(
diff --git a/src/pages/views/workspace/WorkspacePage.tsx b/src/pages/views/workspace/WorkspacePage.tsx
index fcaa23f..1addeac 100644
--- a/src/pages/views/workspace/WorkspacePage.tsx
+++ b/src/pages/views/workspace/WorkspacePage.tsx
@@ -196,6 +196,8 @@ export const WorkspacePage: React.FC = ({ persistentInstance
instanceId={instanceId}
activeWorkflowId={workspace.workflowId}
onSelect={_handleConversationSelect}
+ onCreateNew={workspace.resetToNew}
+ refreshTrigger={workspace.workflowVersion}
/>
)}
{leftTab === 'files' && (
diff --git a/src/pages/views/workspace/useWorkspace.ts b/src/pages/views/workspace/useWorkspace.ts
index f61d02d..0dad3dc 100644
--- a/src/pages/views/workspace/useWorkspace.ts
+++ b/src/pages/views/workspace/useWorkspace.ts
@@ -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([]);
const [pendingEdits, setPendingEdits] = useState([]);
const [workflowId, setWorkflowId] = useState(null);
+ const [workflowVersion, setWorkflowVersion] = useState(0);
const [dataSourceAccesses, setDataSourceAccesses] = useState([]);
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,
diff --git a/src/utils/sseClient.ts b/src/utils/sseClient.ts
index 82e55c7..b3dfc01 100644
--- a/src/utils/sseClient.ts
+++ b/src/utils/sseClient.ts
@@ -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 = {
fileCreated: 'onFileCreated',
dataSourceAccess: 'onDataSourceAccess',
voiceResponse: 'onVoiceResponse',
+ workflowUpdated: 'onWorkflowUpdated',
complete: 'onComplete',
stopped: 'onStopped',
error: 'onError',