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',