From 41a3b8f40eb88d5213cd500f516921f8ec41a0ea Mon Sep 17 00:00:00 2001 From: Ida Dittrich Date: Thu, 21 Aug 2025 18:09:19 +0200 Subject: [PATCH] workflows page --- src/assets/styles/light.css | 16 +- .../Dashboard/DashboardChat/DashboardChat.tsx | 10 +- .../DashboardChat/DashboardChatArea.tsx | 2 - .../DashboardChatAreaConnectedFiles.tsx | 211 +++--------- .../DashboardChat/DashboardChatAreaInput.tsx | 76 +++-- .../DashboardChatAreaLogItem.tsx | 6 - .../DashboardChatAreaMessageItem.tsx | 239 +++++--------- .../DashboardChatAreaMessageList.tsx | 281 +++++++--------- .../DashboardChat.module.css | 2 + .../DashboardChatAreaInput.module.css | 78 ++++- .../DashboardChatConnectedFiles.module.css | 118 +++++++ .../DashboardChatMessages.module.css | 102 +++--- .../dashboardChatAreaProgressBar.ts | 305 ++++++------------ .../DashboardChat/dashboardChatAreaTypes.ts | 36 ++- .../DashboardChat/useWorkflowManager.ts | 18 +- src/components/Dateien/DateienTable.tsx | 57 +++- .../FormGenerator/FormGenerator.module.css | 145 +++++++-- .../FormGenerator/FormGenerator.tsx | 140 +++++--- .../Workflows/WorkflowsTable.module.css | 2 +- src/components/Workflows/WorkflowsTable.tsx | 195 +++++++++-- src/hooks/usePrompts.ts | 28 +- src/hooks/useWorkflows.ts | 18 ++ src/locales/de.ts | 5 +- src/locales/en.ts | 3 + src/locales/fr.ts | 3 + src/pages/Home/Dashboard.tsx | 291 +++++++---------- src/pages/Home/Dateien.tsx | 2 +- .../Home/HomeStyles/Dashboard.module.css | 159 ++++++++- src/pages/Home/HomeStyles/Dateien.module.css | 5 +- 29 files changed, 1439 insertions(+), 1114 deletions(-) create mode 100644 src/components/Dashboard/DashboardChat/DashboardChatAreaStyles/DashboardChatConnectedFiles.module.css diff --git a/src/assets/styles/light.css b/src/assets/styles/light.css index a1ff93f..497bbdf 100644 --- a/src/assets/styles/light.css +++ b/src/assets/styles/light.css @@ -1,7 +1,7 @@ :root { --color-bg: #F8F9FA; /* war vorher surface */ --color-surface: #EFEDE5; /* war vorher bg */ - --color-text: #181818; + --color-text: #3A3A3A; --color-primary: #C7C5B2; --color-primary-hover: #D9D7C6; @@ -19,9 +19,17 @@ --color-secondary-red-hover: #D46872; --color-secondary-red-disabled: #E8B7BA; - --color-gray: #181818; - --color-gray-hover: #2A2A2A; - --color-gray-disabled: #9B9B9B; + --color-gray: #6F7373; + --color-gray-hover: #565A5A; + --color-gray-disabled: #B7BBBA; + + --color-medium-gray: #E0DDD3; + --color-medium-gray-hover: #D1CEC5; + --color-medium-gray-disabled: #E0DDD380; + + --color-highlight-gray: #F5F3ED; + --color-highlight-gray-hover: #E6E3DC; + --color-highlight-gray-disabled: #F5F3ED80; --font-family: "DM Sans", sans-serif; } diff --git a/src/components/Dashboard/DashboardChat/DashboardChat.tsx b/src/components/Dashboard/DashboardChat/DashboardChat.tsx index 3aa0ced..8f698f0 100644 --- a/src/components/Dashboard/DashboardChat/DashboardChat.tsx +++ b/src/components/Dashboard/DashboardChat/DashboardChat.tsx @@ -1,24 +1,16 @@ import React from "react"; -import { Prompt, WorkflowState, WorkflowActions } from "./dashboardChatAreaTypes"; +import { DashboardChatProps } from "./dashboardChatAreaTypes"; import DashboardChatArea from './DashboardChatArea.tsx'; import styles from './DashboardChatAreaStyles/DashboardChat.module.css'; -interface DashboardChatProps { - selectedPrompt?: Prompt | null; - workflowState: WorkflowState; - workflowActions: WorkflowActions; -} - const DashboardChat: React.FC = ({ - selectedPrompt, workflowState, workflowActions }) => { return (
diff --git a/src/components/Dashboard/DashboardChat/DashboardChatArea.tsx b/src/components/Dashboard/DashboardChat/DashboardChatArea.tsx index 82951d1..bdc84fb 100644 --- a/src/components/Dashboard/DashboardChat/DashboardChatArea.tsx +++ b/src/components/Dashboard/DashboardChat/DashboardChatArea.tsx @@ -7,7 +7,6 @@ import { DashboardChatAreaProps } from "./dashboardChatAreaTypes"; import styles from './DashboardChatAreaStyles/DashboardChat.module.css'; const DashboardChatArea: React.FC = ({ - selectedPrompt, workflowState, workflowActions }) => { @@ -32,7 +31,6 @@ const DashboardChatArea: React.FC = ({ {/* Bottom Left: Input Area */}
void; - selectedFile?: FileInfo | null; - attachedFiles?: AttachedFile[]; - onRemoveFile?: (fileId: number) => void; -} +import { ConnectedFilesProps } from './dashboardChatAreaTypes'; +import styles from './DashboardChatAreaStyles/DashboardChatConnectedFiles.module.css'; +import { IoIosAttach } from 'react-icons/io'; const ConnectedFiles: React.FC = ({ onFileSelect, @@ -24,37 +10,16 @@ const ConnectedFiles: React.FC = ({ attachedFiles = [], onRemoveFile }) => { - const [files, setFiles] = useState([]); - const { downloadFile, isDownloading } = useFileDownload(); + const convertedFiles = attachedFiles.map(file => ({ + id: file.id, + name: file.name, + mimeType: file.type, + size: file.size, + creationDate: new Date().toISOString(), + fileData: file.fileData, + objectUrl: file.objectUrl + })); - // Convert attached files to FileInfo format for compatibility with preview - const convertedAttachedFiles = attachedFiles.map(file => { - console.log('ConnectedFiles: Converting attached file:', file.name, 'Has fileData:', !!file.fileData, 'Has objectUrl:', !!file.objectUrl); - return { - id: file.id, - name: file.name, - mimeType: file.type, - size: file.size, - creationDate: new Date().toISOString(), - fileData: file.fileData, - objectUrl: file.objectUrl - }; - }); - - // Combine attached files with workflow files - const allFiles = [...convertedAttachedFiles, ...files]; - - useEffect(() => { - // Could load workflow-specific files here in the future - }, []); - - const getFileIcon = (mimeType: string) => { - if (mimeType.includes('pdf')) return '📄'; - if (mimeType.includes('spreadsheet') || mimeType.includes('excel')) return '📊'; - if (mimeType.startsWith('image/')) return '🖼️'; - if (mimeType.startsWith('text/')) return '📝'; - return '📎'; - }; const formatFileSize = (bytes: number) => { if (bytes < 1024) return bytes + ' B'; @@ -62,142 +27,52 @@ const ConnectedFiles: React.FC = ({ return Math.round(bytes / (1024 * 1024)) + ' MB'; }; - const handleFileClick = (file: any) => { - if (onFileSelect) { - console.log('ConnectedFiles: Selecting file:', file.name, 'Has fileData:', !!file.fileData, 'Has objectUrl:', !!file.objectUrl); - onFileSelect(file); - } - }; - - const handleDownload = async (file: FileInfo) => { - await downloadFile(file.id, file.name); - }; - return ( -
- - {/* Show attached files count */} +
{attachedFiles.length > 0 && ( -
- 📎 {attachedFiles.length} file{attachedFiles.length !== 1 ? 's' : ''} attached for workflow +
+ {attachedFiles.length} file{attachedFiles.length !== 1 ? 's' : ''} attached for workflow
)} - {allFiles.length === 0 ? ( -

+ {convertedFiles.length === 0 ? ( +

No files connected to this workflow

) : ( -
- {allFiles.map((file) => { - const isAttachedFile = attachedFiles.some(af => af.id === file.id); - return ( -
handleFileClick(file)} - style={{ - padding: '12px', - border: `1px solid ${selectedFile?.id === file.id ? 'var(--color-secondary)' : 'var(--color-gray-disabled)'}`, - borderRadius: '8px', - cursor: 'pointer', - backgroundColor: selectedFile?.id === file.id ? 'var(--color-secondary-disabled)' : 'var(--color-bg)', - display: 'flex', - alignItems: 'center', - gap: '12px', - // Highlight attached files - ...(isAttachedFile && { - borderColor: '#1976d2', - backgroundColor: '#f3f8ff' - }) - }} - > - - {getFileIcon(file.mimeType)} - - -
-
- {file.name} - {isAttachedFile && ( - - ATTACHED - - )} -
-
- {file.size ? formatFileSize(file.size) : 'Unknown size'} -
+
+ {convertedFiles.map((file) => ( +
onFileSelect?.(file)} + className={`${styles.fileItem} ${selectedFile?.id === file.id ? styles.selected : ''} ${styles.attached}`} + > + +
+
+ {file.name}
- -
- {isAttachedFile && onRemoveFile && ( - - )} +
+ {file.size ? formatFileSize(file.size) : 'Unknown size'} +
+
+ +
+ {onRemoveFile && ( -
+ )}
- ); - })} +
+ ))}
)}
diff --git a/src/components/Dashboard/DashboardChat/DashboardChatAreaInput.tsx b/src/components/Dashboard/DashboardChat/DashboardChatAreaInput.tsx index b29da89..a44bfd1 100644 --- a/src/components/Dashboard/DashboardChat/DashboardChatAreaInput.tsx +++ b/src/components/Dashboard/DashboardChat/DashboardChatAreaInput.tsx @@ -1,7 +1,8 @@ import React, { useState, useEffect, useRef } from 'react'; import FileAttachmentPopup from './FileAttachmentPopup'; -import { Prompt, WorkflowState, WorkflowActions, InputAreaProps, AttachedFile } from './dashboardChatAreaTypes'; +import { InputAreaProps, AttachedFile } from './dashboardChatAreaTypes'; import { useLanguage } from '../../../contexts/LanguageContext'; +import { useSimplePrompts } from '../../../hooks/usePrompts'; import styles from './DashboardChatAreaStyles/DashboardChatAreaInput.module.css'; import sharedStyles from './DashboardChatAreaStyles/DashboardChat.module.css'; @@ -9,13 +10,13 @@ import sharedStyles from './DashboardChatAreaStyles/DashboardChat.module.css'; const InputArea: React.FC = ({ - selectedPrompt, workflowState, workflowActions, attachedFiles: externalAttachedFiles = [], onAttachedFilesChange }) => { const { t } = useLanguage(); + const { prompts, loading: promptsLoading } = useSimplePrompts(); const [inputValue, setInputValue] = useState(''); const [showFilePopup, setShowFilePopup] = useState(false); const [isSending, setIsSending] = useState(false); @@ -48,12 +49,15 @@ const InputArea: React.FC = ({ textarea.style.height = `${newHeight}px`; }; + // Get the current selected prompt from workflow state + const currentSelectedPrompt = workflowState.selectedPrompt; + // Auto-fill input when prompt is selected useEffect(() => { - if (selectedPrompt) { - setInputValue(selectedPrompt.content); + if (currentSelectedPrompt) { + setInputValue(currentSelectedPrompt.content); } - }, [selectedPrompt]); + }, [currentSelectedPrompt]); // Adjust height when input value changes useEffect(() => { @@ -118,6 +122,24 @@ const InputArea: React.FC = ({ } }; + const handlePromptSelect = (e: React.ChangeEvent) => { + const promptId = e.target.value; + if (promptId === '') { + workflowActions.clearPrompt(); + setInputValue(''); + } else { + const prompt = prompts.find(p => p.id === promptId); + if (prompt) { + workflowActions.selectPrompt(prompt); + } + } + }; + + const handleClearPrompt = () => { + workflowActions.clearPrompt(); + setInputValue(''); + }; + // Drag and drop handlers const handleDragEnter = (e: React.DragEvent) => { e.preventDefault(); @@ -225,12 +247,34 @@ const InputArea: React.FC = ({
)} - {/* Show attached files count */} - {currentAttachedFiles.length > 0 && ( -
- {currentAttachedFiles.length} {currentAttachedFiles.length !== 1 ? t('chat.input.files_attached_plural') : t('chat.input.files_attached')} {t('chat.input.files_attached_label')} + {/* Prompt selection dropdown */} +
+
+ + {currentSelectedPrompt && ( + + )}
- )} +
@@ -251,7 +295,9 @@ const InputArea: React.FC = ({ onBlur={() => setIsFocused(false)} placeholder="" disabled={isSending || shouldShowStopButton} - className={styles.message_textarea} + className={`${styles.message_textarea} ${ + !isFocused && inputValue.trim().length > 0 ? styles.message_textarea_with_content : '' + }`} rows={4} />
@@ -295,17 +341,11 @@ const InputArea: React.FC = ({ {workflowState.currentWorkflowId && !shouldShowStopButton && ( )} - - {selectedPrompt && ( - - {t('chat.input.using_prompt')} {selectedPrompt.name} - - )}
diff --git a/src/components/Dashboard/DashboardChat/DashboardChatAreaLogItem.tsx b/src/components/Dashboard/DashboardChat/DashboardChatAreaLogItem.tsx index a3fb96c..ee0f626 100644 --- a/src/components/Dashboard/DashboardChat/DashboardChatAreaLogItem.tsx +++ b/src/components/Dashboard/DashboardChat/DashboardChatAreaLogItem.tsx @@ -100,12 +100,6 @@ const LogItem: React.FC = ({ log }) => { Source: {log.source}
)} - - {log.progress !== undefined && log.progress >= 0 && ( -
- Progress: {log.progress}% -
- )}
); }; diff --git a/src/components/Dashboard/DashboardChat/DashboardChatAreaMessageItem.tsx b/src/components/Dashboard/DashboardChat/DashboardChatAreaMessageItem.tsx index 3517d0f..2e75440 100644 --- a/src/components/Dashboard/DashboardChat/DashboardChatAreaMessageItem.tsx +++ b/src/components/Dashboard/DashboardChat/DashboardChatAreaMessageItem.tsx @@ -2,6 +2,8 @@ import React from "react"; import { useFileDownload } from "../../../hooks/useWorkflows"; import { Message, Document } from "./dashboardChatAreaTypes"; import messageStyles from './DashboardChatAreaStyles/DashboardChatMessages.module.css'; +import { FaDownload, FaEye } from "react-icons/fa"; + interface MessageItemProps { message: Message; @@ -9,8 +11,7 @@ interface MessageItemProps { onFilePreview?: (file: any) => void; } -// Helper function to format file size -const formatFileSize = (bytes?: number): string => { +const formatFileSize = (bytes?: number) => { if (!bytes) return ''; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(1024)); @@ -18,144 +19,61 @@ const formatFileSize = (bytes?: number): string => { }; -// Helper function to get file icon based on type or extension -const getFileIcon = (type?: string, ext?: string): string => { - // Use extension first if available, then fall back to MIME type - const extension = ext?.toLowerCase(); - const mimeType = type?.toLowerCase(); - - // Check extension first - if (extension) { - // Images - if (['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'].includes(extension)) return '🖼️'; - // Videos - if (['mp4', 'avi', 'mov', 'wmv', 'flv', 'webm', 'mkv'].includes(extension)) return '🎥'; - // Audio - if (['mp3', 'wav', 'aac', 'flac', 'ogg', 'wma'].includes(extension)) return '🎵'; - // Documents - if (extension === 'pdf') return '📕'; - if (['doc', 'docx'].includes(extension)) return '📘'; - if (['xls', 'xlsx'].includes(extension)) return '📊'; - if (['ppt', 'pptx'].includes(extension)) return '📋'; - if (['txt', 'md', 'rtf'].includes(extension)) return '📝'; - // Archives - if (['zip', 'rar', '7z', 'tar', 'gz'].includes(extension)) return '📦'; - // Code files - if (['js', 'ts', 'jsx', 'tsx', 'html', 'css', 'py', 'java', 'cpp', 'c', 'php'].includes(extension)) return '💻'; - } - - // Fall back to MIME type if extension didn't match - if (mimeType) { - if (mimeType.includes('image')) return '🖼️'; - if (mimeType.includes('video')) return '🎥'; - if (mimeType.includes('audio')) return '🎵'; - if (mimeType.includes('pdf')) return '📕'; - if (mimeType.includes('text')) return '📝'; - if (mimeType.includes('word') || mimeType.includes('document')) return '📘'; - if (mimeType.includes('excel') || mimeType.includes('spreadsheet')) return '📊'; - if (mimeType.includes('powerpoint') || mimeType.includes('presentation')) return '📋'; - if (mimeType.includes('zip') || mimeType.includes('archive')) return '📦'; - } - - return '📄'; -}; - const MessageItem: React.FC = ({ message, onFilePreview }) => { const { downloadFile, isDownloading } = useFileDownload(); - - const handleDocumentClick = (document: Document) => { - // If there's a downloadUrl, use it; otherwise try the url - const downloadLink = document.downloadUrl || document.url; - - if (downloadLink) { - // Open the document in a new tab - window.open(downloadLink, '_blank'); - } + const handleDocumentClick = (doc: Document) => { + const link = doc.downloadUrl || doc.url; + if (link) window.open(link, '_blank'); }; - const handlePreview = (document: Document, e: React.MouseEvent) => { + const handlePreview = (doc: Document, e: React.MouseEvent) => { e.stopPropagation(); + const fileId = doc.fileId || parseInt(doc.id || '0'); + if (!fileId || isNaN(fileId)) return; - - // Use fileId if available, otherwise try to use id as fallback - const fileId = document.fileId || parseInt(document.id || '0'); - - if (!fileId || isNaN(fileId)) { - console.error('❌ Invalid file ID for preview:', document); - return; - } - - - // Call the parent callback to show preview in the file preview quadrant if (onFilePreview) { onFilePreview({ id: fileId.toString(), - name: document.name, - mimeType: document.type || 'application/octet-stream', - size: document.size, - fileId: fileId + name: doc.name, + mimeType: doc.type || 'application/octet-stream', + size: doc.size, + fileId }); } }; - const handleDownload = async (document: Document, e: React.MouseEvent) => { + const handleDownload = async (doc: Document, e: React.MouseEvent) => { e.stopPropagation(); + const fileId = doc.fileId || parseInt(doc.id || '0'); + if (!fileId) return; - console.log(`⬇️ Download requested for:`, document); - - // Use fileId if available, otherwise try to use id as fallback - const fileId = document.fileId || parseInt(document.id || '0'); - - if (!fileId) { - console.error('❌ No file ID for download:', document); - return; - } - - // Construct filename with extension if available - const fileName = document.ext ? `${document.name}.${document.ext}` : document.name; - + const fileName = doc.ext ? `${doc.name}.${doc.ext}` : doc.name; await downloadFile(fileId, fileName); }; - // Debug: Log document check before rendering - const hasDocuments = message.documents && message.documents.length > 0; + const hasDocuments = message.documents?.length > 0; - - // Format timestamp - const formatTimestamp = (timestamp?: string) => { - if (!timestamp) { - return ''; - } - - const date = new Date(timestamp); - - if (isNaN(date.getTime())) { - return ''; - } + const formatTimestamp = (ts?: string) => { + if (!ts) return ''; + const date = new Date(ts); + if (isNaN(date.getTime())) return ''; const now = new Date(); const isToday = date.toDateString() === now.toDateString(); - let formatted = ''; - if (isToday) { - formatted = date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); - } else { - formatted = date.toLocaleDateString([], { month: 'short', day: 'numeric' }) + ' ' + - date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); - } - - return formatted; + return isToday + ? date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) + : date.toLocaleDateString([], { month: 'short', day: 'numeric' }) + ' ' + + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); }; + const isUser = message.role === 'user'; + return ( -
+
- {message.role === 'user' ? 'You' : message.agentName} + {isUser ? 'You' : message.agentName} {message.timestamp && ( • {formatTimestamp(message.timestamp)} @@ -163,68 +81,57 @@ const MessageItem: React.FC = ({ message, onFilePreview }) => )}
-
+
- {message.content || ( - - [No message content] - - )} + {message.content || [No message content]}
{hasDocuments && ( -
-
- 📎 {message.role === 'user' ? 'Uploaded' : 'Attached'} Files ({message.documents!.length}) +
+
+ {isUser ? 'Uploaded' : 'Attached'} Files ({message.documents?.length || 0})
- {message.documents!.map((document, docIndex) => { - return ( -
handleDocumentClick(document)} - title={`Click to open ${document.name}`} - > - - {getFileIcon(document.type, document.ext)} - -
-
- {document.ext ? `${document.name}.${document.ext}` : document.name} + {message.documents!.map((doc, i) => ( +
handleDocumentClick(doc)} + title={`Click to open ${doc.name}`} + > +
+
+ {doc.ext ? `${doc.name}.${doc.ext}` : doc.name} +
+ {doc.size && ( +
+ {formatFileSize(doc.size)}
- {document.size && ( -
- {formatFileSize(document.size)} -
- )} -
-
- - -
+ )}
- ); - })} +
+ + +
+
+ ))}
)} diff --git a/src/components/Dashboard/DashboardChat/DashboardChatAreaMessageList.tsx b/src/components/Dashboard/DashboardChat/DashboardChatAreaMessageList.tsx index e78269e..f26e04f 100644 --- a/src/components/Dashboard/DashboardChat/DashboardChatAreaMessageList.tsx +++ b/src/components/Dashboard/DashboardChat/DashboardChatAreaMessageList.tsx @@ -3,245 +3,194 @@ import { MessageListProps } from './dashboardChatAreaTypes'; import { useApiRequest } from '../../../hooks/useApi'; import MessageItem from './DashboardChatAreaMessageItem'; import LogItem from './DashboardChatAreaLogItem'; -import { - calculateWorkflowProgress, - mergeMessagesAndLogs, - transformWorkflowMessage -} from './dashboardChatAreaProgressBar'; +import { mergeMessagesAndLogs, transformWorkflowMessage } from './dashboardChatAreaProgressBar'; import messageStyles from './DashboardChatAreaStyles/DashboardChatMessages.module.css'; import { IoIosArrowDown, IoIosChatbubbles } from 'react-icons/io'; import { useLanguage } from '../../../contexts/LanguageContext'; - - -const MessageList: React.FC = ({ - workflowState, - onFilePreview -}) => { +const MessageList: React.FC = ({ workflowState, onFilePreview }) => { const { t } = useLanguage(); const { request } = useApiRequest(); - const [transformedMessages, setTransformedMessages] = React.useState([]); - const [isTransforming, setIsTransforming] = React.useState(false); - const scrollContainerRef = React.useRef(null); - const [isUserScrolledUp, setIsUserScrolledUp] = React.useState(false); - const lastMessageCountRef = React.useRef(0); - const [showProgressBar, setShowProgressBar] = React.useState(false); + const [messages, setMessages] = React.useState([]); + const [isLoading, setIsLoading] = React.useState(false); + const scrollRef = React.useRef(null); + const [isScrolledUp, setIsScrolledUp] = React.useState(false); + const lastCountRef = React.useRef(0); + const [showProgress, setShowProgress] = React.useState(false); const [progressText, setProgressText] = React.useState(''); - const [progressPercentage, setProgressPercentage] = React.useState(0); - const [maxCompletedTasks, setMaxCompletedTasks] = React.useState(0); + const [progressPercent, setProgressPercent] = React.useState(0); + const [maxTasks, setMaxTasks] = React.useState(0); - // ONE single effect to handle all progress logic React.useEffect(() => { - const pendingMessages = workflowState.pendingMessages || []; + const pending = workflowState.pendingMessages || []; const logs = workflowState.logs || []; - // Show progress bar if user sent message or logs exist - if ((pendingMessages.length > 0 || logs.length > 0) && !showProgressBar) { - setShowProgressBar(true); - setProgressPercentage(0); - setMaxCompletedTasks(0); + if ((pending.length > 0 || logs.length > 0) && !showProgress) { + setShowProgress(true); + setProgressPercent(0); + setMaxTasks(0); } - // Update text based on current state - if (logs.length === 0 && pendingMessages.length > 0) { + if (logs.length === 0 && pending.length > 0) { setProgressText(t('chat.messages.loading_progress')); return; } if (logs.length > 0) { - let totalTasks = 0; - let completedTasks = 0; + let total = 0; + let completed = 0; logs.forEach(log => { - const message = log.message || ''; + const msg = log.message || ''; + const startMatch = msg.match(/Executing task (\d+)\/(\d+)/i); + if (startMatch) total = Math.max(total, parseInt(startMatch[2])); - const taskStartMatch = message.match(/Executing task (\d+)\/(\d+)/i); - if (taskStartMatch) { - totalTasks = Math.max(totalTasks, parseInt(taskStartMatch[2])); - } - - const taskCompleteMatch = message.match(/✅.*Task (\d+).*completed/i); - if (taskCompleteMatch) { - completedTasks = Math.max(completedTasks, parseInt(taskCompleteMatch[1])); - } + const completeMatch = msg.match(/✅.*Task (\d+).*completed/i); + if (completeMatch) completed = Math.max(completed, parseInt(completeMatch[1])); }); - if (totalTasks > 0) { - const currentPercentage = Math.round((completedTasks / totalTasks) * 100); - setProgressText(`${completedTasks}/${totalTasks} ${t('chat.messages.tasks')} (${currentPercentage}%)`); + if (total > 0) { + const percent = Math.round((completed / total) * 100); + setProgressText(`${completed}/${total} ${t('chat.messages.tasks')} (${percent}%)`); - // ONLY update percentage if we completed more tasks than before - if (completedTasks > maxCompletedTasks) { - setMaxCompletedTasks(completedTasks); - setProgressPercentage(currentPercentage); + if (completed > maxTasks) { + setMaxTasks(completed); + setProgressPercent(percent); } } else { setProgressText(`${t('chat.messages.analyzing_workflow')} (${logs.length} ${t('chat.messages.logs')})`); } } - }, [workflowState.pendingMessages, workflowState.logs, showProgressBar, maxCompletedTasks, t]); + }, [workflowState.pendingMessages, workflowState.logs, showProgress, maxTasks, t]); - // Clear progress bar when workflow changes React.useEffect(() => { if (!workflowState.currentWorkflowId) { - setShowProgressBar(false); + setShowProgress(false); setProgressText(''); - setProgressPercentage(0); - setMaxCompletedTasks(0); + setProgressPercent(0); + setMaxTasks(0); } }, [workflowState.currentWorkflowId]); const timeline = React.useMemo(() => { - return mergeMessagesAndLogs(transformedMessages, workflowState.logs || []); - }, [transformedMessages, workflowState.logs]); + return mergeMessagesAndLogs(messages, workflowState.logs || []); + }, [messages, workflowState.logs]); - // Transform messages when workflow messages change React.useEffect(() => { - const transformMessages = async () => { - // Use pending messages for immediate display, backend messages for confirmed content - // Since the workflow manager already filters out duplicate user messages, - // we need to combine them properly: pending first, then backend messages - const pendingMessages = workflowState.pendingMessages || []; - const backendMessages = workflowState.messages || []; + const transform = async () => { + const pending = workflowState.pendingMessages || []; + const backend = workflowState.messages || []; + const all = [...pending, ...backend]; - - - // Create a simple combined list - pending messages will be user messages, - // backend messages will be mostly assistant messages (user messages filtered out) - const allMessages = [...pendingMessages, ...backendMessages]; - - - - if (allMessages.length === 0) { - setTransformedMessages([]); + if (all.length === 0) { + setMessages([]); return; } - setIsTransforming(true); + setIsLoading(true); try { - const transformed = await Promise.all( - allMessages.map(msg => transformWorkflowMessage(msg, request)) - ); - setTransformedMessages(transformed); - } catch (error) { - console.error('Error transforming messages:', error); - setTransformedMessages([]); + const transformed = await Promise.all(all.map(msg => transformWorkflowMessage(msg, request))); + setMessages(transformed); + } catch { + setMessages([]); } finally { - setIsTransforming(false); + setIsLoading(false); } }; - - transformMessages(); + transform(); }, [workflowState.messages, workflowState.pendingMessages, request]); - // Scroll handling - const checkScrollPosition = React.useCallback(() => { - const container = scrollContainerRef.current; - if (!container) return; - - const { scrollTop, scrollHeight, clientHeight } = container; - const distanceFromBottom = scrollHeight - scrollTop - clientHeight; - setIsUserScrolledUp(distanceFromBottom > 100); + const checkScroll = React.useCallback(() => { + const el = scrollRef.current; + if (!el) return; + const { scrollTop, scrollHeight, clientHeight } = el; + setIsScrolledUp(scrollHeight - scrollTop - clientHeight > 100); }, []); const scrollToBottom = React.useCallback(() => { - const container = scrollContainerRef.current; - if (container) { - container.scrollTop = container.scrollHeight; - } + const el = scrollRef.current; + if (el) el.scrollTop = el.scrollHeight; }, []); - // Auto-scroll on new messages React.useEffect(() => { - const currentCount = timeline.length; - const hasNewItems = currentCount > lastMessageCountRef.current; - - if (hasNewItems && !isUserScrolledUp) { + const count = timeline.length; + if (count > lastCountRef.current && !isScrolledUp) { setTimeout(scrollToBottom, 100); } - - lastMessageCountRef.current = currentCount; - }, [timeline.length, isUserScrolledUp, scrollToBottom]); + lastCountRef.current = count; + }, [timeline.length, isScrolledUp, scrollToBottom]); - const { currentWorkflowId, isLoading, error } = workflowState; - - const isEmpty = timeline.length === 0 && !isLoading && !isTransforming && !currentWorkflowId; + const { currentWorkflowId, isLoading: workflowLoading, error } = workflowState; + const isEmpty = timeline.length === 0 && !workflowLoading && !isLoading && !currentWorkflowId; return (
-
- {error && ( -
- Error: {error} -
- )} + onScroll={checkScroll} + > + {error &&
Error: {error}
} -
- {timeline.map((timelineItem, index) => { - const { type, item } = timelineItem; - - if (type === 'message') { - return ( - - ); - } else if (type === 'log') { - return ( - - ); - } - return null; - })} -
+
+ {timeline.map((item, index) => { + if (item.type === 'message') { + return ( + + ); + } else if (item.type === 'log') { + return ( + + ); + } + return null; + })} +
{isEmpty && ( -
-
- +
+
+ +
+

{t('chat.messages.no_workflow_selected')}

+

{t('chat.messages.no_workflow_selected_description')}

-

{t('chat.messages.no_workflow_selected')}

-

{t('chat.messages.no_workflow_selected_description')}

-
- )} -
- - {/* Progress Bar */} - {showProgressBar && ( + )} +
+ + {showProgress && (
-
- {t('chat.messages.workflow_progress')} +
+ {t('chat.messages.workflow_progress')} {progressText} -
-
-
-
+
+
+
+
)} - + onClick={scrollToBottom} + title={t('chat.messages.scroll_to_bottom_btn')} + > + +
); }; diff --git a/src/components/Dashboard/DashboardChat/DashboardChatAreaStyles/DashboardChat.module.css b/src/components/Dashboard/DashboardChat/DashboardChatAreaStyles/DashboardChat.module.css index 69fb345..9308f2b 100644 --- a/src/components/Dashboard/DashboardChat/DashboardChatAreaStyles/DashboardChat.module.css +++ b/src/components/Dashboard/DashboardChat/DashboardChatAreaStyles/DashboardChat.module.css @@ -54,6 +54,8 @@ .connected_files_quadrant { grid-row: 2; grid-column: 2; + max-height: 200px; /* Fixed height for the connected files area */ + overflow: hidden; /* Ensure no overflow at quadrant level */ } /* Chat Messages styles moved to DashboardChatMessages.module.css */ diff --git a/src/components/Dashboard/DashboardChat/DashboardChatAreaStyles/DashboardChatAreaInput.module.css b/src/components/Dashboard/DashboardChat/DashboardChatAreaStyles/DashboardChatAreaInput.module.css index 6484a8e..b27a8e3 100644 --- a/src/components/Dashboard/DashboardChat/DashboardChatAreaStyles/DashboardChatAreaInput.module.css +++ b/src/components/Dashboard/DashboardChat/DashboardChatAreaStyles/DashboardChatAreaInput.module.css @@ -26,6 +26,73 @@ margin-bottom: 12px; } +/* Prompt Selection Styles */ +.prompt_selection_container { + margin-bottom: 12px; +} + +.prompt_dropdown_wrapper { + display: flex; + align-items: center; + gap: 8px; +} + +.prompt_dropdown { + flex: 1; + padding: 10px 16px; + padding-right: 40px; /* Space for the dropdown arrow */ + border: 1px solid var(--color-primary); + border-radius: 25px; + font-size: 14px; + font-family: var(--font-family); + background: var(--color-bg); + color: var(--color-text); + opacity: 0.8; + transition: border-color 0.2s ease, opacity 0.2s ease; + cursor: pointer; + appearance: none; /* Remove default dropdown arrow */ + background-image: url("data:image/svg+xml;charset=US-ASCII,"); background-repeat: no-repeat; + background-position: right 16px center; /* Position the custom arrow */ + background-size: 12px; +} + +.prompt_dropdown:focus { + outline: none; + border-color: var(--color-secondary); + opacity: 1; +} + +.prompt_dropdown:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.clear_prompt_button { + width: 32px; + height: 32px; + border: 1px solid var(--color-primary); + border-radius: 50%; + background: var(--color-bg); + color: var(--color-gray); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + font-size: 14px; + transition: all 0.2s ease; +} + +.clear_prompt_button:hover { + background: var(--color-bg); + color: var(--color-secondary); + border: 1px solid var(--color-secondary); +} + +.clear_prompt_button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + .attached_files_count { margin-bottom: 8px; padding: 6px 10px; @@ -114,10 +181,17 @@ opacity: 0.6; } +.message_textarea_with_content { + opacity: 0.9; + border-color: var(--color-secondary); + background: var(--color-bg); +} + .input_actions_row { display: flex; gap: 8px; align-items: center; + justify-content: flex-end; } @@ -131,10 +205,6 @@ color: var(--color-gray); } -.prompt_indicator { - font-size: 12px; - color: var(--color-gray); -} /* Drag and Drop Styles */ .input_area_container { diff --git a/src/components/Dashboard/DashboardChat/DashboardChatAreaStyles/DashboardChatConnectedFiles.module.css b/src/components/Dashboard/DashboardChat/DashboardChatAreaStyles/DashboardChatConnectedFiles.module.css new file mode 100644 index 0000000..60e6037 --- /dev/null +++ b/src/components/Dashboard/DashboardChat/DashboardChatAreaStyles/DashboardChatConnectedFiles.module.css @@ -0,0 +1,118 @@ +.container { + padding: 16px; + height: 100%; + max-height: 100%; + overflow: hidden; + box-sizing: border-box; + display: flex; + flex-direction: column; +} + +.attachedInfo { + margin-bottom: 12px; + padding: 8px 15px; + background-color: var(--color-secondary); + border-radius: 25px; + font-size: 14px; + color: var(--color-bg); + font-weight: 500; + flex-shrink: 0; + font-family: var(--font-family); + display: flex; + align-items: center; + gap: 4px; +} + +.attachedInfoIcon { + width: 1.2rem; + height: 1.2rem; +} + +.emptyState { + color: var(--color-gray); + text-align: center; + flex: 1; + display: flex; + align-items: center; + justify-content: center; +} + +.filesList { + display: flex; + flex-direction: column; + gap: 8px; + overflow-y: auto; + flex: 1; + min-height: 0; +} + +.fileItem { + font-family: var(--font-family); + font-weight: 500; + padding: 10px 15px; + border: 1px solid var(--color-primary); + border-radius: 25px; + cursor: pointer; + background-color: var(--color-bg); + display: flex; + align-items: center; + gap: 12px; + color: var(--color-text); +} + +.fileInfo { + flex: 1; + min-width: 0; +} + +.fileName { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + display: flex; + align-items: center; + gap: 6px; + font-family: var(--font-family); + font-weight: 500; + color: var(--color-text); +} + +.fileSize { + font-size: 12px; + color: var(--color-gray); +} + +.fileActions { + display: flex; + gap: 4px; +} + +.removeButton { + padding: 4px 8px; + font-size: 12px; + background-color: transparent; + border: 1px solid var(--color-secondary); + border-radius: 15px; + cursor: pointer; + color: var(--color-secondary); + transition: all 0.3s ease; +} + +.removeButton:hover { + padding: 4px 8px; + font-size: 12px; + background-color: var(--color-secondary); + border: 1px solid var(--color-secondary); + border-radius: 15px; + cursor: pointer; + color: white; +} + +.downloadButton { + padding: 4px 8px; + font-size: 12px; + background-color: transparent; + border: 1px solid var(--color-gray-disabled); + border-radius: 4px; + cursor: pointer; +} diff --git a/src/components/Dashboard/DashboardChat/DashboardChatAreaStyles/DashboardChatMessages.module.css b/src/components/Dashboard/DashboardChat/DashboardChatAreaStyles/DashboardChatMessages.module.css index 5d09d66..8b31f71 100644 --- a/src/components/Dashboard/DashboardChat/DashboardChatAreaStyles/DashboardChatMessages.module.css +++ b/src/components/Dashboard/DashboardChat/DashboardChatAreaStyles/DashboardChatMessages.module.css @@ -90,7 +90,7 @@ display: flex; flex-direction: column; max-width: 80%; - min-width: 10%; + min-width: 0%; margin-bottom: 4px; position: relative; } @@ -136,10 +136,10 @@ .message_bubble { padding: 12px 16px; - border-radius: 18px; + border-radius: 25px; position: relative; word-wrap: break-word; - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); + box-shadow: 1px 1px 6px rgba(0, 0, 0, 0.1); width: 100%; } @@ -152,9 +152,8 @@ /* Assistant message bubble */ .message_bubble.assistant { - background-color: var(--color-surface); + background-color: var(--color-highlight-gray); color: var(--color-text); - border: 1px solid var(--color-gray-disabled); border-bottom-left-radius: 4px; } @@ -189,24 +188,16 @@ .message_documents { margin-top: 8px; padding: 8px; - background-color: rgba(0, 0, 0, 0.05); - border-radius: 12px; - border: 1px solid rgba(0, 0, 0, 0.1); + background-color: color-mix(in srgb, var(--color-primary), transparent 80%); + border-radius: 15px; + border: 1px solid var(--color-primary); width: 100%; box-sizing: border-box; } -/* Document styling for user messages */ -.message_documents.user { - background-color: rgba(255, 255, 255, 0.15); - border: 1px solid rgba(255, 255, 255, 0.2); -} -/* Document styling for assistant messages */ -.message_documents.assistant { - background-color: var(--color-bg); - border: 1px solid var(--color-gray-disabled); -} + + .message_documents_header { font-size: 12px; @@ -228,17 +219,16 @@ .message_document_item { display: flex; align-items: center; - gap: 8px; + gap: 9px; padding: 6px; border-radius: 4px; - background-color: var(--color-surface); + background-color: var(--color-highlight-gray); margin-bottom: 4px; cursor: pointer; + color: var(--color-text); } -.message_document_icon { - font-size: 16px; -} + .message_document_info { flex: 1; @@ -259,16 +249,28 @@ .message_document_actions { display: flex; - gap: 4px; + gap: 9px; } -.message_document_action { - padding: 4px 8px; + + +.message_document_action_button { + + width: 33px; + height: 32px; font-size: 12px; - background-color: transparent; - border: 1px solid var(--color-gray-disabled); - border-radius: 4px; + background-color: var(--color-secondary); + border: 1px solid var(--color-secondary); + border-radius: 25px; cursor: pointer; + display: flex; + align-items: center; + justify-content: center; +} + +.message_document_action_icon { + font-size: 16px; + color: white; } .message_document_action:hover { @@ -386,16 +388,17 @@ 100% { transform: translateX(100%); } } -/* Workflow Progress Bar (fixed at bottom, outside scrollable area) */ .workflow_progress_container { flex-shrink: 0; padding: 12px 16px; - background-color: var(--color-surface); - border-top: 1px solid var(--color-gray-disabled); + border-top: 1px solid var(--color-primary); + border-bottom: 1px solid var(--color-primary); margin-top: auto; + margin-right: 15px; } .workflow_progress_label { + font-family: var(--font-family); font-size: 12px; color: var(--color-text); margin-bottom: 8px; @@ -457,32 +460,13 @@ /* Workflow Log Messages */ .log_container { margin-top: 8px; - padding: 8px 12px; - border-radius: 6px; + padding: 20px; + border-radius: 25px; font-size: 12px; font-family: 'Courier New', monospace; border-left: 3px solid; - background-color: rgba(0, 0, 0, 0.02); -} - -.log_container.info { - border-left-color: var(--color-secondary); - background-color: rgba(59, 130, 246, 0.05); -} - -.log_container.warning { - border-left-color: #f59e0b; - background-color: rgba(245, 158, 11, 0.05); -} - -.log_container.error { - border-left-color: #ef4444; - background-color: rgba(239, 68, 68, 0.05); -} - -.log_container.debug { - border-left-color: var(--color-gray); - background-color: rgba(107, 114, 128, 0.05); + border: 1px solid var(--color-gray-disabled); + background-color: var(--color-bg); } .log_header { @@ -502,15 +486,19 @@ } .log_level.info { + background-color: var(--color-gray); +} + +.log_level.success { background-color: var(--color-secondary); } .log_level.warning { - background-color: #f59e0b; + background-color: var(--color-primary); } .log_level.error { - background-color: #ef4444; + background-color: var(--color-red); } .log_level.debug { diff --git a/src/components/Dashboard/DashboardChat/dashboardChatAreaProgressBar.ts b/src/components/Dashboard/DashboardChat/dashboardChatAreaProgressBar.ts index d3aab75..c9154f3 100644 --- a/src/components/Dashboard/DashboardChat/dashboardChatAreaProgressBar.ts +++ b/src/components/Dashboard/DashboardChat/dashboardChatAreaProgressBar.ts @@ -1,276 +1,155 @@ import { WorkflowLog, WorkflowProgress, TimelineItem } from './dashboardChatAreaTypes'; -// Helper function to parse task and action progress from log messages -const parseLogProgress = (logMessage: string) => { - const message = logMessage.trim(); +const parseLogProgress = (msg: string) => { + const m = msg.trim(); - // Task start: "Executing task X/Y" - const taskStartMatch = message.match(/^Executing task (\d+)\/(\d+)$/i); - if (taskStartMatch) { - return { - taskNumber: parseInt(taskStartMatch[1]), - totalTasks: parseInt(taskStartMatch[2]), - type: 'task_start' as const - }; - } + const taskStart = m.match(/^Executing task (\d+)\/(\d+)$/i); + if (taskStart) return { taskNumber: parseInt(taskStart[1]), totalTasks: parseInt(taskStart[2]), type: 'task_start' as const }; - // Action start: "Task X - Starting action Y/Z" - const actionStartMatch = message.match(/^Task (\d+) - Starting action (\d+)\/(\d+)$/i); - if (actionStartMatch) { - return { - taskNumber: parseInt(actionStartMatch[1]), - actionNumber: parseInt(actionStartMatch[2]), - totalActions: parseInt(actionStartMatch[3]), - type: 'action_start' as const - }; - } + const actionStart = m.match(/^Task (\d+) - Starting action (\d+)\/(\d+)$/i); + if (actionStart) return { taskNumber: parseInt(actionStart[1]), actionNumber: parseInt(actionStart[2]), totalActions: parseInt(actionStart[3]), type: 'action_start' as const }; - // Action complete: "✅ Task X - Action Y/Z completed" - const actionCompleteMatch = message.match(/^(?:✅\s+)?Task (\d+) - Action (\d+)\/(\d+) completed$/i); - if (actionCompleteMatch) { - return { - taskNumber: parseInt(actionCompleteMatch[1]), - actionNumber: parseInt(actionCompleteMatch[2]), - totalActions: parseInt(actionCompleteMatch[3]), - isCompleted: true, - type: 'action_complete' as const - }; - } + const actionComplete = m.match(/^(?:✅\s+)?Task (\d+) - Action (\d+)\/(\d+) completed$/i); + if (actionComplete) return { taskNumber: parseInt(actionComplete[1]), actionNumber: parseInt(actionComplete[2]), totalActions: parseInt(actionComplete[3]), isCompleted: true, type: 'action_complete' as const }; - // Task complete: "🎯 Task X/Y completed" - const taskCompleteMatch = message.match(/^(?:🎯\s+)?Task (\d+)\/(\d+) completed$/i); - if (taskCompleteMatch) { - return { - taskNumber: parseInt(taskCompleteMatch[1]), - totalTasks: parseInt(taskCompleteMatch[2]), - isCompleted: true, - type: 'task_complete' as const - }; - } + const taskComplete = m.match(/^(?:🎯\s+)?Task (\d+)\/(\d+) completed$/i); + if (taskComplete) return { taskNumber: parseInt(taskComplete[1]), totalTasks: parseInt(taskComplete[2]), isCompleted: true, type: 'task_complete' as const }; return { type: 'unknown' as const }; }; -// Calculate workflow progress from messages and logs export const calculateWorkflowProgress = (messages: any[], logs: WorkflowLog[] = []): WorkflowProgress | null => { if (messages.length === 0 && logs.length === 0) return null; - // Check if waiting for assistant response const lastMessage = messages[messages.length - 1]; - if (lastMessage?.role === 'user') { - return { current: 0, total: 0, percentage: 0, isLoading: true }; - } + if (lastMessage?.role === 'user') return { current: 0, total: 0, percentage: 0, isLoading: true }; - // Parse logs for task structure let totalTasks = 0; - const taskActionCounts: { [taskNumber: number]: { total: number; completedActions: Set } } = {}; + const taskCounts: { [key: number]: { total: number; completed: Set } } = {}; - // Analyze logs - logs.forEach((log) => { + logs.forEach(log => { if (!log.message) return; + const p = parseLogProgress(log.message); - const progress = parseLogProgress(log.message); + if (p.type === 'task_start' && p.totalTasks) totalTasks = Math.max(totalTasks, p.totalTasks); - if (progress.type === 'task_start' && progress.totalTasks) { - totalTasks = Math.max(totalTasks, progress.totalTasks); + if (p.type === 'action_start' && p.taskNumber && p.totalActions) { + if (!taskCounts[p.taskNumber]) taskCounts[p.taskNumber] = { total: 0, completed: new Set() }; + taskCounts[p.taskNumber].total = Math.max(taskCounts[p.taskNumber].total, p.totalActions); } - if (progress.type === 'action_start' && progress.taskNumber && progress.totalActions) { - const taskNum = progress.taskNumber; - if (!taskActionCounts[taskNum]) { - taskActionCounts[taskNum] = { total: 0, completedActions: new Set() }; - } - taskActionCounts[taskNum].total = Math.max(taskActionCounts[taskNum].total, progress.totalActions); + if (p.type === 'action_complete' && p.taskNumber && p.actionNumber && p.totalActions) { + if (!taskCounts[p.taskNumber]) taskCounts[p.taskNumber] = { total: p.totalActions, completed: new Set() }; + taskCounts[p.taskNumber].completed.add(p.actionNumber); + taskCounts[p.taskNumber].total = Math.max(taskCounts[p.taskNumber].total, p.totalActions); } - if (progress.type === 'action_complete' && progress.taskNumber && progress.actionNumber && progress.totalActions) { - const taskNum = progress.taskNumber; - if (!taskActionCounts[taskNum]) { - taskActionCounts[taskNum] = { total: progress.totalActions, completedActions: new Set() }; - } - taskActionCounts[taskNum].completedActions.add(progress.actionNumber); - taskActionCounts[taskNum].total = Math.max(taskActionCounts[taskNum].total, progress.totalActions); - } - - if (progress.type === 'task_complete' && progress.taskNumber && progress.totalTasks) { - const taskNum = progress.taskNumber; - totalTasks = Math.max(totalTasks, progress.totalTasks); - // Mark all actions complete for this task - if (taskActionCounts[taskNum]) { - const task = taskActionCounts[taskNum]; - for (let i = 1; i <= task.total; i++) { - task.completedActions.add(i); - } + if (p.type === 'task_complete' && p.taskNumber && p.totalTasks) { + totalTasks = Math.max(totalTasks, p.totalTasks); + if (taskCounts[p.taskNumber]) { + const task = taskCounts[p.taskNumber]; + for (let i = 1; i <= task.total; i++) task.completed.add(i); } } }); - // Analyze messages for additional action completion patterns - messages.forEach((message) => { - if (message.role !== 'assistant' || !message.content) return; - - const actionCompleteMatch = message.content.match(/✅\s+Task (\d+) - Action\s+(?:(\d+)\/(\d+)|.*completed)/i); - if (actionCompleteMatch) { - const taskNumber = parseInt(actionCompleteMatch[1]); - - if (actionCompleteMatch[2] && actionCompleteMatch[3]) { - const actionNumber = parseInt(actionCompleteMatch[2]); - const totalActions = parseInt(actionCompleteMatch[3]); - - if (!taskActionCounts[taskNumber]) { - taskActionCounts[taskNumber] = { total: totalActions, completedActions: new Set() }; - } - taskActionCounts[taskNumber].completedActions.add(actionNumber); - taskActionCounts[taskNumber].total = Math.max(taskActionCounts[taskNumber].total, totalActions); + messages.forEach(msg => { + if (msg.role !== 'assistant' || !msg.content) return; + const match = msg.content.match(/✅\s+Task (\d+) - Action\s+(?:(\d+)\/(\d+)|.*completed)/i); + if (match) { + const taskNum = parseInt(match[1]); + if (match[2] && match[3]) { + const actionNum = parseInt(match[2]); + const totalActions = parseInt(match[3]); + if (!taskCounts[taskNum]) taskCounts[taskNum] = { total: totalActions, completed: new Set() }; + taskCounts[taskNum].completed.add(actionNum); + taskCounts[taskNum].total = Math.max(taskCounts[taskNum].total, totalActions); } else { - // Named action without numbers - treat as 1/1 - if (!taskActionCounts[taskNumber]) { - taskActionCounts[taskNumber] = { total: 1, completedActions: new Set() }; - } - taskActionCounts[taskNumber].completedActions.add(1); - if (taskActionCounts[taskNumber].total === 0) { - taskActionCounts[taskNumber].total = 1; - } + if (!taskCounts[taskNum]) taskCounts[taskNum] = { total: 1, completed: new Set() }; + taskCounts[taskNum].completed.add(1); + if (taskCounts[taskNum].total === 0) taskCounts[taskNum].total = 1; } } }); - // Calculate completed tasks - let completedTasks = 0; - for (let taskNum = 1; taskNum <= totalTasks; taskNum++) { - const task = taskActionCounts[taskNum]; - if (task && task.total > 0 && task.completedActions.size === task.total) { - completedTasks++; - } + let completed = 0; + for (let i = 1; i <= totalTasks; i++) { + const task = taskCounts[i]; + if (task && task.total > 0 && task.completed.size === task.total) completed++; } - if (totalTasks > 0) { - const percentage = Math.round((completedTasks / totalTasks) * 100); - return { - current: completedTasks, - total: totalTasks, - percentage: percentage, - isLoading: false - }; - } - - return null; + return totalTasks > 0 ? { current: completed, total: totalTasks, percentage: Math.round((completed / totalTasks) * 100), isLoading: false } : null; }; -// Safe date parsing with fallback -export const safeParseDate = (timestamp: any, fallback: number = Date.now()): Date => { - if (!timestamp) return new Date(fallback); +export const safeParseDate = (ts: any, fallback = Date.now()): Date => { + if (!ts) return new Date(fallback); - let dateToTry = timestamp; - - if (typeof timestamp === 'number') { - dateToTry = timestamp < 10000000000 ? timestamp * 1000 : timestamp; - } else if (typeof timestamp === 'string' && /^\d+$/.test(timestamp)) { - const numericTimestamp = parseInt(timestamp); - dateToTry = numericTimestamp < 10000000000 ? numericTimestamp * 1000 : numericTimestamp; - } else if (timestamp instanceof Date) { - dateToTry = timestamp; + let d = ts; + if (typeof ts === 'number') d = ts < 10000000000 ? ts * 1000 : ts; + else if (typeof ts === 'string' && /^\d+$/.test(ts)) { + const n = parseInt(ts); + d = n < 10000000000 ? n * 1000 : n; } - const date = new Date(dateToTry); + const date = new Date(d); return isNaN(date.getTime()) ? new Date(fallback) : date; }; -// Merge and sort messages and logs by timestamp export const mergeMessagesAndLogs = (messages: any[], logs: WorkflowLog[]): TimelineItem[] => { - const combined: TimelineItem[] = []; + const items: TimelineItem[] = []; - // Add messages - messages.forEach((message, index) => { - const rawTimestamp = message.timestamp || message.publishedAt; - const fallbackTime = Date.now() - ((messages.length + logs.length) - index) * 1000; - const timestamp = safeParseDate(rawTimestamp, fallbackTime); - - combined.push({ - type: 'message', - item: message, - timestamp - }); + messages.forEach((msg, i) => { + const ts = safeParseDate(msg.timestamp || msg.publishedAt, Date.now() - ((messages.length + logs.length) - i) * 1000); + items.push({ type: 'message', item: msg, timestamp: ts }); }); - // Add logs - logs.forEach((log, index) => { - const fallbackTime = Date.now() - ((logs.length) - index) * 1000; - const timestamp = safeParseDate(log.timestamp, fallbackTime); - - combined.push({ - type: 'log', - item: log, - timestamp - }); + logs.forEach((log, i) => { + const ts = safeParseDate(log.timestamp, Date.now() - (logs.length - i) * 1000); + items.push({ type: 'log', item: log, timestamp: ts }); }); - // Sort by timestamp - combined.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime()); - - return combined; + return items.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime()); }; -// Transform workflow message to display message -export const transformWorkflowMessage = async (workflowMessage: any, request: any): Promise => { - let documents: any[] = []; +export const transformWorkflowMessage = async (msg: any, request: any): Promise => { + let docs: any[] = []; - // Handle documents - if (workflowMessage.documents && workflowMessage.documents.length > 0) { - documents = workflowMessage.documents.map((doc: any) => ({ - id: doc.id || doc.fileId, - fileId: typeof doc.fileId === 'string' ? parseInt(doc.fileId) : doc.fileId, - name: doc.filename, - ext: doc.filename.split('.').pop() || 'unknown', - type: doc.mimeType, - size: doc.fileSize, - downloadUrl: `/api/workflows/files/${doc.fileId}/download` + if (msg.documents?.length > 0) { + docs = msg.documents.map((d: any) => ({ + id: d.id || d.fileId, + fileId: typeof d.fileId === 'string' ? parseInt(d.fileId) : d.fileId, + name: d.filename, + ext: d.filename.split('.').pop() || 'unknown', + type: d.mimeType, + size: d.fileSize, + downloadUrl: `/api/workflows/files/${d.fileId}/download` })); - } else if (workflowMessage.fileIds && workflowMessage.fileIds.length > 0) { - // Legacy fileIds approach - const documentPromises = workflowMessage.fileIds.map(async (fileId: number) => { + } else if (msg.fileIds?.length > 0) { + const promises = msg.fileIds.map(async (id: number) => { try { - const response = await request({ - url: `/api/workflows/files/${fileId}/preview`, - method: 'get' - }); - + const res = await request({ url: `/api/workflows/files/${id}/preview`, method: 'get' }); return { - id: fileId.toString(), - fileId: fileId, - name: response.name || response.fileName || `File_${fileId}`, - ext: response.extension || response.ext || (response.name ? response.name.split('.').pop() : 'txt'), - type: response.mimeType || response.type || 'application/octet-stream', - size: response.size || 0, - downloadUrl: response.downloadUrl || response.url - }; - } catch (error) { - return { - id: fileId.toString(), - fileId: fileId, - name: `File_${fileId}`, - ext: 'unknown', - type: 'application/octet-stream', - size: 0 + id: id.toString(), + fileId: id, + name: res.name || res.fileName || `File_${id}`, + ext: res.extension || res.ext || (res.name ? res.name.split('.').pop() : 'txt'), + type: res.mimeType || res.type || 'application/octet-stream', + size: res.size || 0, + downloadUrl: res.downloadUrl || res.url }; + } catch { + return { id: id.toString(), fileId: id, name: `File_${id}`, ext: 'unknown', type: 'application/octet-stream', size: 0 }; } }); - - documents = await Promise.all(documentPromises); + docs = await Promise.all(promises); } - const content = workflowMessage.message || - workflowMessage.content || - (workflowMessage as any).text || - (workflowMessage as any).body || - ''; - return { - id: workflowMessage.id, - role: workflowMessage.role, - agentName: workflowMessage.role === 'user' ? 'You' : 'Assistant', - content: content, - timestamp: workflowMessage.publishedAt || workflowMessage.timestamp, - documents: documents + id: msg.id, + role: msg.role, + agentName: msg.role === 'user' ? 'You' : 'Assistant', + content: msg.message || msg.content || msg.text || msg.body || '', + timestamp: msg.publishedAt || msg.timestamp, + documents: docs }; }; diff --git a/src/components/Dashboard/DashboardChat/dashboardChatAreaTypes.ts b/src/components/Dashboard/DashboardChat/dashboardChatAreaTypes.ts index 51fa05c..60ba0df 100644 --- a/src/components/Dashboard/DashboardChat/dashboardChatAreaTypes.ts +++ b/src/components/Dashboard/DashboardChat/dashboardChatAreaTypes.ts @@ -1,14 +1,19 @@ // Simplified types - everything in one place export interface Prompt { - id: number; + id: string; + mandateId: string; name: string; content: string; createdAt?: string; isShared?: boolean; } +export interface DashboardChatProps { + workflowState: WorkflowState; + workflowActions: WorkflowActions; +} + export interface DashboardChatAreaProps { - selectedPrompt?: Prompt | null; workflowState: WorkflowState; workflowActions: WorkflowActions; } @@ -126,6 +131,7 @@ export interface WorkflowState { logs: WorkflowLog[]; isLoading: boolean; error: string | null; + selectedPrompt: Prompt | null; } export interface WorkflowActions { @@ -134,6 +140,8 @@ export interface WorkflowActions { continueWorkflow: (prompt: string, fileIds?: number[]) => Promise; stopWorkflow: () => Promise; clearWorkflow: () => void; + selectPrompt: (prompt: Prompt | null) => void; + clearPrompt: () => void; } export interface FileInfo { @@ -169,21 +177,27 @@ export interface Message { } export interface InputAreaProps { - selectedPrompt?: Prompt | null; workflowState: WorkflowState; workflowActions: WorkflowActions; attachedFiles?: AttachedFile[]; onAttachedFilesChange?: (files: AttachedFile[]) => void; } - export interface AttachedFile { - id: number; - name: string; - size: number; - type: string; - fileData?: File; - objectUrl?: string; - } +export interface AttachedFile { + id: number; + name: string; + size: number; + type: string; + fileData?: File; + objectUrl?: string; +} + +export interface ConnectedFilesProps { + onFileSelect?: (file: FileInfo) => void; + selectedFile?: FileInfo | null; + attachedFiles?: AttachedFile[]; + onRemoveFile?: (fileId: number) => void; +} export interface MessageListProps { workflowState: WorkflowState; diff --git a/src/components/Dashboard/DashboardChat/useWorkflowManager.ts b/src/components/Dashboard/DashboardChat/useWorkflowManager.ts index 4f9b1f3..db13dab 100644 --- a/src/components/Dashboard/DashboardChat/useWorkflowManager.ts +++ b/src/components/Dashboard/DashboardChat/useWorkflowManager.ts @@ -8,6 +8,7 @@ export function useWorkflowManager(initialWorkflowId?: string | null): [Workflow const [isPolling, setIsPolling] = useState(false); const [pendingMessages, setPendingMessages] = useState([]); const [sentUserMessages, setSentUserMessages] = useState>(new Set()); // Track sent user messages + const [selectedPrompt, setSelectedPrompt] = useState(null); // Selected prompt state const pollingIntervalRef = useRef(null); // Hook-based data fetching @@ -177,6 +178,14 @@ export function useWorkflowManager(initialWorkflowId?: string | null): [Workflow setSentUserMessages(new Set()); }, []); + const selectPrompt = useCallback((prompt: any | null) => { + setSelectedPrompt(prompt); + }, []); + + const clearPrompt = useCallback(() => { + setSelectedPrompt(null); + }, []); + // Only clear pending messages when workflow changes to a different ID // (not when creating a new workflow) const previousWorkflowId = useRef(currentWorkflowId); @@ -228,7 +237,8 @@ export function useWorkflowManager(initialWorkflowId?: string | null): [Workflow pendingMessages, logs: logs || [], isLoading, - error + error, + selectedPrompt }; const actions: WorkflowActions = useMemo(() => ({ @@ -236,8 +246,10 @@ export function useWorkflowManager(initialWorkflowId?: string | null): [Workflow startNewWorkflow, continueWorkflow, stopWorkflow, - clearWorkflow - }), [loadWorkflow, startNewWorkflow, continueWorkflow, stopWorkflow, clearWorkflow]); + clearWorkflow, + selectPrompt, + clearPrompt + }), [loadWorkflow, startNewWorkflow, continueWorkflow, stopWorkflow, clearWorkflow, selectPrompt, clearPrompt]); return [state, actions]; } diff --git a/src/components/Dateien/DateienTable.tsx b/src/components/Dateien/DateienTable.tsx index 8f58406..c65ca9e 100644 --- a/src/components/Dateien/DateienTable.tsx +++ b/src/components/Dateien/DateienTable.tsx @@ -3,6 +3,7 @@ import { useLanguage } from '../../contexts/LanguageContext'; import { Popup, EditForm } from '../Popup'; import styles from './DateienTable.module.css'; import { useDateienLogic } from './dateienLogic.tsx'; +import { useFileOperations, type UserFile } from '../../hooks/useFiles'; import type { DateienTableProps } from './dateienInterfaces'; export function DateienTable({ className = '' }: DateienTableProps) { @@ -19,8 +20,58 @@ export function DateienTable({ className = '' }: DateienTableProps) { editingFile, editFileFields, handleSaveFile, - handleCancelEdit + handleCancelEdit, + refetch } = useDateienLogic(); + + // Use file operations for delete functionality + const { handleFileDelete } = useFileOperations(); + + // Handle single file deletion + const handleDeleteSingle = async (file: UserFile) => { + const fileName = file.file_name || file.id; + if (window.confirm(t('files.delete.confirm', 'Are you sure you want to delete "{name}"?').replace('{name}', fileName))) { + const success = await handleFileDelete(file.id, () => { + // Optimistic update - this will be called immediately + refetch(); + }); + + if (!success) { + console.error('Delete failed for file:', file.id); + // Refetch to restore the file in case of failure + refetch(); + } + } + }; + + // Handle multiple file deletion + const handleDeleteMultiple = async (filesToDelete: UserFile[]) => { + const fileCount = filesToDelete.length; + if (window.confirm(t('files.delete.confirmMultiple', 'Are you sure you want to delete {count} files?').replace('{count}', fileCount.toString()))) { + // Start all delete operations simultaneously + const deletePromises = filesToDelete.map(async (file) => { + try { + const success = await handleFileDelete(file.id); + return { fileId: file.id, success }; + } catch (error) { + console.error('Failed to delete file:', file.id, error); + return { fileId: file.id, success: false }; + } + }); + + // Wait for all deletions to complete + const results = await Promise.all(deletePromises); + + // Check if any deletions failed + const failedDeletions = results.filter(result => !result.success); + if (failedDeletions.length > 0) { + console.error('Some file deletions failed:', failedDeletions); + } + + // Refresh the file list regardless of individual failures + refetch(); + } + }; // Show error state if (error) { @@ -41,7 +92,6 @@ export function DateienTable({ className = '' }: DateienTableProps) { diff --git a/src/components/FormGenerator/FormGenerator.module.css b/src/components/FormGenerator/FormGenerator.module.css index 91c9a67..bb16f9f 100644 --- a/src/components/FormGenerator/FormGenerator.module.css +++ b/src/components/FormGenerator/FormGenerator.module.css @@ -14,6 +14,64 @@ margin-bottom: 10px; } +/* Integrated Delete Controls - appears inside the controls container */ +.deleteControlsIntegrated { + display: flex; + align-items: center; + gap: 12px; + flex-shrink: 0; +} + +.deleteButton, +.deleteAllButton { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 16px; + border: none; + border-radius: 20px; + font-size: 14px; + font-family: var(--font-family); + cursor: pointer; + transition: all 0.2s ease; + font-weight: 500; + color: white; + background: var(--color-secondary); +} + +.deleteButton { + background: var(--color-secondary); + color: white; +} + +.deleteButton:hover { + background: var(--color-secondary-hover); + transform: translateY(-1px); +} + +.deleteAllButton { + background: var(--color-secondary); + color: white; +} + +.deleteAllButton:hover { + background: var(--color-secondary-hover); + transform: translateY(-1px); +} + +.deleteIcon { + font-size: 16px; + font-weight: bold; +} + +.selectionInfo { + color: var(--color-text); + font-size: 14px; + font-family: var(--font-family); + margin-left: auto; + opacity: 0.8; +} + /* Controls Section */ .controls { display: flex; @@ -220,7 +278,7 @@ border: 1px solid var(--color-primary); border-radius: 25px; background: var(--color-bg); - max-height: 90%; + max-height: 70%; } .loading { @@ -253,7 +311,6 @@ user-select: none; position: relative; z-index: 10; - } .th.actionsColumn { @@ -303,6 +360,7 @@ vertical-align: middle; } + .tr { transition: background-color 0.2s ease; } @@ -323,36 +381,61 @@ .selectColumn { text-align: center; padding: 8px !important; + background: var(--color-bg); + position: relative; +} + +/* Selection Column border only on body cells, not header */ +tbody .selectColumn { + border-top: 1px solid var(--color-primary); } .selectColumn input[type="checkbox"] { cursor: pointer; - transform: scale(1.2); + transform: scale(1.3); + width: 16px; + height: 16px; + accent-color: var(--color-secondary); + margin: 0; + padding: 0; + border: 2px solid var(--color-primary); + border-radius: 3px; + background: var(--color-bg); + position: relative; + z-index: 1; + appearance: auto; + -webkit-appearance: checkbox; + -moz-appearance: checkbox; } -/* Actions Column - Resizable like other columns */ +.selectColumn input[type="checkbox"]:checked { + background-color: var(--color-secondary); + border-color: var(--color-secondary); +} + +.selectColumn input[type="checkbox"]:hover { + border-color: var(--color-secondary); +} + +.selectColumn input[type="checkbox"]:focus { + outline: 2px solid var(--color-secondary); + outline-offset: 2px; +} + +/* Actions Column - Fixed width like select column */ .actionsColumn { white-space: nowrap; - text-align: left; + text-align: center; padding: 8px !important; font-weight: 400; box-sizing: border-box; + background: var(--color-bg); + position: relative; } - - -/* Actions Column header */ -thead .actionsColumn { - text-align: center; - padding: 8px !important; -} - - - /* Actions Column border only on body cells, not header */ tbody .actionsColumn { border-top: 1px solid var(--color-primary); - text-align: center; } .actionButtons { @@ -469,6 +552,25 @@ tbody .actionsColumn { /* Responsive Design */ @media (max-width: 768px) { + .deleteControlsIntegrated { + flex-direction: column; + align-items: stretch; + gap: 10px; + width: 100%; + } + + .deleteButton, + .deleteAllButton { + justify-content: center; + padding: 10px 16px; + } + + .selectionInfo { + text-align: center; + margin-left: 0; + margin-top: 5px; + } + .controls { flex-direction: column; align-items: stretch; @@ -503,7 +605,7 @@ tbody .actionsColumn { } .tableContainer { - max-height: 400px; + max-height: 90%px; } .th, @@ -560,10 +662,17 @@ tbody .actionsColumn { .paginationButton:focus, .searchInput:focus, .filterInput:focus, -.filterSelect:focus { +.filterSelect:focus, +.deleteButton:focus, +.deleteAllButton:focus { outline: none; } +.deleteButton:focus, +.deleteAllButton:focus { + box-shadow: 0 0 0 2px rgba(220, 53, 69, 0.5); +} + /* Custom scrollbar for table container */ .tableContainer::-webkit-scrollbar { width: 8px; diff --git a/src/components/FormGenerator/FormGenerator.tsx b/src/components/FormGenerator/FormGenerator.tsx index 6c74231..aa20456 100644 --- a/src/components/FormGenerator/FormGenerator.tsx +++ b/src/components/FormGenerator/FormGenerator.tsx @@ -38,13 +38,14 @@ export interface FormGeneratorProps { onClick: (row: T) => void; icon?: string | React.ReactNode | ((row: T) => React.ReactNode); }[]; + onDelete?: (row: T) => void; + onDeleteMultiple?: (rows: T[]) => void; className?: string; } export function FormGenerator>({ data, columns: providedColumns, - title, searchable = true, filterable = true, sortable = true, @@ -55,9 +56,11 @@ export function FormGenerator>({ showPageSizeSelector = true, onRowClick, onRowSelect, - selectable = false, + selectable = true, // Default to true for selection functionality loading = false, actions = [], + onDelete, + onDeleteMultiple, className = '' }: FormGeneratorProps) { const { t } = useLanguage(); @@ -116,17 +119,12 @@ export function FormGenerator>({ useEffect(() => { const initialWidths: Record = {}; - // Add actions column if present - if (actions.length > 0) { - initialWidths['actions'] = 120; // Default width for actions column - } - detectedColumns.forEach(col => { // Set a default width if none specified to ensure all columns have explicit widths initialWidths[col.key] = col.width || 150; }); setColumnWidths(initialWidths); - }, [detectedColumns, actions]); + }, [detectedColumns]); // Filter and search data const filteredData = useMemo(() => { @@ -262,6 +260,34 @@ export function FormGenerator>({ } }; + // Handle delete single item + const handleDeleteSingle = (row: T, index: number) => { + if (onDelete) { + onDelete(row); + // Remove from selection if it was selected + if (selectedRows.has(index)) { + const newSelected = new Set(selectedRows); + newSelected.delete(index); + setSelectedRows(newSelected); + if (onRowSelect) { + const selectedData = Array.from(newSelected).map(i => paginatedData[i]); + onRowSelect(selectedData); + } + } + } + }; + + // Handle delete multiple items + const handleDeleteMultiple = () => { + if (onDeleteMultiple && selectedRows.size > 0) { + const selectedData = Array.from(selectedRows).map(i => paginatedData[i]); + onDeleteMultiple(selectedData); + // Clear selection + setSelectedRows(new Set()); + onRowSelect?.([]); + } + }; + // Handle page size change const handlePageSizeChange = (newPageSize: number) => { setCurrentPageSize(newPageSize); @@ -291,8 +317,8 @@ export function FormGenerator>({ const tableContainer = tableRef.current?.parentElement; if (tableContainer) { const containerWidth = tableContainer.clientWidth; - const actionsColumnWidth = 0; // Actions column is now resizable like other columns - const selectColumnWidth = selectable ? 40 : 0; + const actionsColumnWidth = actions.length > 0 ? 120 : 0; // Fixed width actions column + const selectColumnWidth = selectable ? 50 : 0; // Fixed width select column const fixedWidth = actionsColumnWidth + selectColumnWidth; // Calculate total width of all OTHER data columns (excluding the one being resized) @@ -345,9 +371,41 @@ export function FormGenerator>({ return (
- {(searchable || filterable) && ( + {(searchable || filterable || (selectable && selectedRows.size > 0)) && (
- {searchable && ( + {/* Delete Controls - Show when items are selected */} + {selectable && selectedRows.size > 0 && ( +
+ {selectedRows.size === 1 && onDelete && ( + + )} + {selectedRows.size > 1 && onDeleteMultiple && ( + + )} + +
+ )} + + {/* Search Controls - Hide when items are selected */} + {searchable && selectedRows.size === 0 && (
>({ - {actions.length > 0 && ( - - )} {selectable && ( - )} + {actions.length > 0 && ( + + )} {detectedColumns.map(column => ( + )} + {actions.length > 0 && ( )} - {selectable && ( - - )} {detectedColumns.map(column => (
- - {resizable && ( -
handleMouseDown(e, 'actions')} - /> - )} -
+ 0} onChange={handleSelectAll} + title="Select all items" /> + + >({ className={`${styles.tr} ${selectedRows.has(index) ? styles.selected : ''} ${onRowClick ? styles.clickable : ''}`} onClick={() => onRowClick?.(row, index)} > - {actions.length > 0 && ( + {selectable && ( + + handleRowSelect(index)} + onClick={(e) => e.stopPropagation()} + title="Select this item" + /> +
{actions.map((action, actionIndex) => ( @@ -618,16 +674,6 @@ export function FormGenerator>({
- handleRowSelect(index)} - onClick={(e) => e.stopPropagation()} - /> - (null); + + // Configure edit fields for workflow name editing + const editWorkflowFields: EditFieldConfig[] = useMemo(() => [ + { + key: 'name', + label: t('workflows.field.name', 'Workflow Name'), + type: 'string', + editable: true, + required: true, + validator: (value: string) => { + if (!value || value.trim() === '') { + return t('workflows.validation.nameRequired', 'Workflow name cannot be empty'); + } + if (value.length > 100) { + return t('workflows.validation.nameTooLong', 'Workflow name cannot exceed 100 characters'); + } + return null; + } + } + ], [t]); + // Configure columns for the workflows table const columns: ColumnConfig[] = useMemo(() => [ { @@ -46,9 +80,9 @@ function WorkflowsTable({ className = '' }: WorkflowsTableProps) { sortable: true, filterable: true, searchable: true, - formatter: (value: string | undefined, row: Workflow) => ( + formatter: (value: string | undefined) => ( - {value || row.title || t('workflows.unnamed')} + {value || t('workflows.unnamed')} ) }, @@ -139,15 +173,9 @@ function WorkflowsTable({ className = '' }: WorkflowsTableProps) { ], [t]); // Handle workflow actions - const handleStopWorkflow = async (workflow: Workflow) => { - const success = await stopWorkflow(workflow.id); - if (success) { - refetch(); // Refresh the workflows list - } - }; const handleDeleteWorkflow = async (workflow: Workflow) => { - const workflowName = workflow.name || workflow.title || workflow.id; + const workflowName = workflow.name || workflow.id; if (window.confirm(t('workflows.delete.confirm').replace('{name}', workflowName))) { const success = await deleteWorkflow(workflow.id); if (success) { @@ -156,35 +184,126 @@ function WorkflowsTable({ className = '' }: WorkflowsTableProps) { } }; + // Handle single workflow deletion for bulk delete + const handleDeleteSingle = async (workflow: Workflow) => { + const workflowName = workflow.name || workflow.id; + if (window.confirm(t('workflows.delete.confirm', 'Are you sure you want to delete "{name}"?').replace('{name}', workflowName))) { + const success = await deleteWorkflow(workflow.id); + if (success) { + refetch(); // Refresh the workflows list + } else { + console.error('Delete failed for workflow:', workflow.id); + } + } + }; + + // Handle multiple workflow deletion + const handleDeleteMultiple = async (workflowsToDelete: Workflow[]) => { + const workflowCount = workflowsToDelete.length; + if (window.confirm(t('workflows.delete.confirmMultiple', 'Are you sure you want to delete {count} workflows?').replace('{count}', workflowCount.toString()))) { + // Start all delete operations simultaneously + const deletePromises = workflowsToDelete.map(async (workflow) => { + try { + const success = await deleteWorkflow(workflow.id); + return { workflowId: workflow.id, success }; + } catch (error) { + console.error('Failed to delete workflow:', workflow.id, error); + return { workflowId: workflow.id, success: false }; + } + }); + + // Wait for all deletions to complete + const results = await Promise.all(deletePromises); + + // Check if any deletions failed + const failedDeletions = results.filter(result => !result.success); + if (failedDeletions.length > 0) { + console.error('Some workflow deletions failed:', failedDeletions); + } + + // Refresh the workflow list regardless of individual failures + refetch(); + } + }; + + // Handle edit workflow + const handleEditWorkflow = (workflow: Workflow) => { + setEditingWorkflow(workflow); + setEditModalOpen(true); + }; + + // Handle save workflow + const handleSaveWorkflow = async (updatedWorkflow: Workflow) => { + if (!editingWorkflow) return; + + try { + // Call API to update workflow name + const result = await updateWorkflow(editingWorkflow.id, { + name: updatedWorkflow.name + }); + + if (result.success) { + // Close modal + setEditModalOpen(false); + setEditingWorkflow(null); + + // Refresh workflow list + await refetch(); + } else { + console.error('Failed to update workflow:', result.error); + // TODO: Show error message to user + } + } catch (error) { + console.error('Failed to update workflow:', error); + // TODO: Show error message to user + } + }; + + // Handle cancel edit + const handleCancelEdit = () => { + setEditModalOpen(false); + setEditingWorkflow(null); + }; + + // Handle play workflow - navigate to dashboard with workflow ID + const handlePlayWorkflow = (workflow: Workflow) => { + // Navigate to dashboard with workflow ID as URL parameter + navigate(`/dashboard?workflowId=${workflow.id}`); + }; + // Configure action buttons const actions = useMemo(() => [ { - label: t('workflows.action.stop'), - icon: (row: Workflow) => { - const isStoppingThis = stoppingWorkflows.has(row.id); - if (isStoppingThis) return '⏳'; - return '⏹️'; + label: t('workflows.action.play'), + icon: (_row: Workflow) => { + return ; }, onClick: (row: Workflow) => { - if (row.status === 'running' && !stoppingWorkflows.has(row.id)) { - handleStopWorkflow(row); - } + handlePlayWorkflow(row); + } + }, + { + label: t('workflows.action.edit'), + icon: (_row: Workflow) => { + return ; + }, + onClick: (row: Workflow) => { + handleEditWorkflow(row); } }, { label: t('workflows.action.delete'), - icon: (row: Workflow) => { - const isDeletingThis = deletingWorkflows.has(row.id); - if (isDeletingThis) return '⏳'; - return '🗑️'; + icon: (_row: Workflow) => { + return ; }, onClick: (row: Workflow) => { if (!deletingWorkflows.has(row.id)) { handleDeleteWorkflow(row); } } - } - ], [t, stoppingWorkflows, deletingWorkflows, handleStopWorkflow, handleDeleteWorkflow]); + }, + + ], [t, deletingWorkflows, handleDeleteWorkflow, handleEditWorkflow, handlePlayWorkflow]); // Show error state if (error) { @@ -205,7 +324,6 @@ function WorkflowsTable({ className = '' }: WorkflowsTableProps) { { // TODO: Navigate to workflow detail view console.log('Clicked workflow:', workflow); }} /> + + {/* Edit Workflow Modal */} + + {editingWorkflow && ( + + )} + ); } diff --git a/src/hooks/usePrompts.ts b/src/hooks/usePrompts.ts index d4ca03a..266d96e 100644 --- a/src/hooks/usePrompts.ts +++ b/src/hooks/usePrompts.ts @@ -3,7 +3,8 @@ import { useApiRequest } from './useApi'; // Prompt interfaces export interface Prompt { - id: number; + id: string; + mandateId: string; name: string; content: string; createdAt?: string; @@ -25,6 +26,31 @@ export interface ShareRequest { title?: string; } +// Simple prompts list hook for dropdown +export function useSimplePrompts() { + const [prompts, setPrompts] = useState([]); + const { request, isLoading: loading, error } = useApiRequest(); + + const fetchPrompts = async () => { + try { + const data = await request({ + url: '/api/prompts', + method: 'get' + }); + + setPrompts(data || []); + } catch (error) { + // Error is already handled by useApiRequest + } + }; + + useEffect(() => { + fetchPrompts(); + }, []); + + return { prompts, loading, error, refetch: fetchPrompts }; +} + // Prompts list hook export function usePrompts() { const [prompts, setPrompts] = useState([]); diff --git a/src/hooks/useWorkflows.ts b/src/hooks/useWorkflows.ts index 44672de..811e859 100644 --- a/src/hooks/useWorkflows.ts +++ b/src/hooks/useWorkflows.ts @@ -118,6 +118,23 @@ export function useWorkflowOperations() { } }; + 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, @@ -128,6 +145,7 @@ export function useWorkflowOperations() { startWorkflow, stopWorkflow, deleteWorkflow, + updateWorkflow, isLoading }; } diff --git a/src/locales/de.ts b/src/locales/de.ts index 860b3a2..fc40871 100644 --- a/src/locales/de.ts +++ b/src/locales/de.ts @@ -204,8 +204,11 @@ export default { 'chat.input.stopping': 'Wird gestoppt...', 'chat.input.drop_files_here': 'Dateien hier ablegen zum Anhängen', 'chat.input.drop_disabled': 'Datei-Ablage während Workflow deaktiviert', - 'chat.input.new_chat': 'Neuer Chat', + 'chat.input.new_chat': 'Chat leeren...', 'chat.input.using_prompt': 'Verwende Vorlage:', + 'chat.input.select_prompt': 'Prompt auswählen...', + 'chat.input.loading_prompts': 'Prompts werden geladen...', + 'chat.input.clear_prompt': 'Prompt löschen', // File Preview 'file_preview.loading': 'Vorschau wird geladen...', diff --git a/src/locales/en.ts b/src/locales/en.ts index 80be524..d1d3002 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -207,6 +207,9 @@ export default { 'chat.input.drop_disabled': 'File drop disabled during workflow', 'chat.input.new_chat': 'New Chat', 'chat.input.using_prompt': 'Using prompt:', + 'chat.input.select_prompt': 'Select a prompt...', + 'chat.input.loading_prompts': 'Loading prompts...', + 'chat.input.clear_prompt': 'Clear prompt', // File Preview 'file_preview.loading': 'Loading preview...', diff --git a/src/locales/fr.ts b/src/locales/fr.ts index 3d9496e..fcea583 100644 --- a/src/locales/fr.ts +++ b/src/locales/fr.ts @@ -206,6 +206,9 @@ export default { 'chat.input.drop_disabled': 'Dépôt de fichiers désactivé pendant le workflow', 'chat.input.new_chat': 'Nouveau Chat', 'chat.input.using_prompt': 'Utilisation du modèle:', + 'chat.input.select_prompt': 'Sélectionner un prompt...', + 'chat.input.loading_prompts': 'Chargement des prompts...', + 'chat.input.clear_prompt': 'Effacer le prompt', // File Preview 'file_preview.loading': 'Chargement de l\'aperçu...', diff --git a/src/pages/Home/Dashboard.tsx b/src/pages/Home/Dashboard.tsx index cb5faa3..6dd5709 100644 --- a/src/pages/Home/Dashboard.tsx +++ b/src/pages/Home/Dashboard.tsx @@ -1,37 +1,39 @@ import { useState, useCallback, useRef, useEffect } from 'react'; -import { IoMdRefresh, IoMdArrowDropdown } from 'react-icons/io'; +import { useSearchParams } from 'react-router-dom'; +import { IoMdArrowDropdown } from 'react-icons/io'; import { useLanguage } from '../../contexts/LanguageContext'; import { useWorkflows } from '../../hooks/useWorkflows'; -import { Prompt, Workflow } from '../../components/Dashboard/DashboardChat/dashboardChatAreaTypes'; +import { Workflow } from '../../components/Dashboard/DashboardChat/dashboardChatAreaTypes'; import { useWorkflowManager } from '../../components/Dashboard/DashboardChat/useWorkflowManager'; import styles from './HomeStyles/Dashboard.module.css' import sharedStyles from '../../components/PageManager/pages.module.css'; - import DashboardChat from '../../components/Dashboard/DashboardChat/DashboardChat'; +import { IoMdClose } from 'react-icons/io'; -function Dashboard () { +function Dashboard() { const { t } = useLanguage(); - const [selectedPrompt, setSelectedPrompt] = useState(null); + const [searchParams, setSearchParams] = useSearchParams(); const [isDropdownOpen, setIsDropdownOpen] = useState(false); const dropdownRef = useRef(null); - // Central workflow management - const [workflowState, workflowActions] = useWorkflowManager(); - - // Fetch workflows for dropdown + // Get workflow ID from URL parameters + const workflowIdFromUrl = searchParams.get('workflowId'); + const [workflowState, workflowActions] = useWorkflowManager(workflowIdFromUrl); const { workflows, loading: workflowsLoading, error: workflowsError } = useWorkflows(); const handleWorkflowSelect = useCallback((workflowId: string) => { workflowActions.loadWorkflow(workflowId); setIsDropdownOpen(false); - }, [workflowActions]); + // Clear the URL parameter once workflow is loaded + setSearchParams({}); + }, [workflowActions, setSearchParams]); const handleResetWorkflow = useCallback(() => { workflowActions.clearWorkflow(); - setSelectedPrompt(null); - }, [workflowActions]); + // Clear the URL parameter when resetting + setSearchParams({}); + }, [workflowActions, setSearchParams]); - // Close dropdown when clicking outside useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { @@ -47,23 +49,18 @@ function Dashboard () { const formatWorkflowId = (id: string) => `${id.substring(0, 8)}...`; - // Format workflow ID for display const displayWorkflowId = workflowState.currentWorkflowId ? `${workflowState.currentWorkflowId.substring(0, 8)}...` : t('dashboard.log.no_workflow'); - // Calculate workflow stats const getWorkflowStats = () => { if (!workflowState.workflow) return null; - const workflow = workflowState.workflow; - const messages = workflowState.messages; // Use messages from workflowState, not workflow.messages + const { workflow, messages } = workflowState; const messageCount = messages?.length || 0; - const fileCount = messages?.reduce((count, msg) => { - return count + (msg.documents?.length || msg.fileIds?.length || 0); - }, 0) || 0; + const fileCount = messages?.reduce((count, msg) => + count + (msg.documents?.length || msg.fileIds?.length || 0), 0) || 0; - // Aggregate stats from workflow stats and message stats const totalTokens = (workflow.stats?.tokenCount || 0) + (messages?.reduce((sum, msg) => sum + (msg.stats?.tokenCount || 0), 0) || 0); const totalBytesSent = (workflow.stats?.bytesSent || 0) + @@ -73,7 +70,6 @@ function Dashboard () { const totalErrors = (workflow.stats?.errorCount || 0) + (messages?.reduce((sum, msg) => sum + (msg.stats?.errorCount || 0), 0) || 0); - // Calculate overall success rate const successfulMessages = messages?.filter(msg => msg.success)?.length || 0; const successRate = messageCount > 0 ? (successfulMessages / messageCount) * 100 : 100; @@ -99,9 +95,7 @@ function Dashboard () { return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; }; - const formatDate = (dateString: string) => { - return new Date(dateString).toLocaleString(); - }; + const formatDate = (dateString: string) => new Date(dateString).toLocaleString(); const getStatusColor = (status: string) => { switch (status.toLowerCase()) { @@ -127,118 +121,109 @@ function Dashboard () { } }; - const formatStatus = (status: string) => { - return status.charAt(0).toUpperCase() + status.slice(1).toLowerCase(); - }; + const formatStatus = (status: string) => + status.charAt(0).toUpperCase() + status.slice(1).toLowerCase(); + + const stats = getWorkflowStats(); return (
- {/* Vertical Divider - spans from after title to bottom */}

{t('nav.dashboard')}

-
+
{workflowState.currentWorkflowId ? ( <> - {(() => { - const stats = getWorkflowStats(); - return ( -
-
- {t('dashboard.stats.workflow')} - {displayWorkflowId} +
+
+ {t('dashboard.stats.workflow')} + {displayWorkflowId} +
+ + {stats && ( + <> +
+ +
+ {t('dashboard.stats.status')} + + {formatStatus(stats.status)} +
- {stats && ( - <> -
- -
- {t('dashboard.stats.status')} - {formatStatus(stats.status)} -
- -
- {t('dashboard.stats.rounds')} - {stats.rounds} -
- -
- {t('dashboard.stats.messages')} - {stats.messageCount} -
- -
- {t('dashboard.stats.files')} - {stats.fileCount} -
- -
- {t('dashboard.stats.tokens')} - {stats.tokenCount.toLocaleString()} -
- -
- {t('dashboard.stats.data_sent')} - {formatBytes(stats.bytesSent)} -
- -
- {t('dashboard.stats.data_received')} - {formatBytes(stats.bytesReceived)} -
- -
- {t('dashboard.stats.success_rate')} - = 90 ? 'var(--color-success)' : - stats.successRate >= 70 ? 'var(--color-warning)' : 'var(--color-error)' - }}>{stats.successRate}% -
- -
- {t('dashboard.stats.errors')} - 0 ? 'var(--color-error)' : 'var(--color-text)' }}>{stats.errorCount} -
- -
- {t('dashboard.stats.started')} - {formatDate(stats.startedAt)} -
- - )} -
- ); - })()} +
+ {t('dashboard.stats.rounds')} + {stats.rounds} +
+ +
+ {t('dashboard.stats.messages')} + {stats.messageCount} +
+ +
+ {t('dashboard.stats.files')} + {stats.fileCount} +
+ +
+ {t('dashboard.stats.tokens')} + {stats.tokenCount.toLocaleString()} +
+ +
+ {t('dashboard.stats.data_sent')} + {formatBytes(stats.bytesSent)} +
+ +
+ {t('dashboard.stats.data_received')} + {formatBytes(stats.bytesReceived)} +
+ +
+ {t('dashboard.stats.success_rate')} + = 90 ? 'var(--color-success)' : + stats.successRate >= 70 ? 'var(--color-warning)' : 'var(--color-error)' + }}>{stats.successRate}% +
+ +
+ {t('dashboard.stats.errors')} + 0 ? styles.statValueError : ''}`}> + {stats.errorCount} + +
+ +
+ {t('dashboard.stats.started')} + + {formatDate(stats.startedAt)} + +
+ + )} +
) : ( -
+
{isDropdownOpen && !workflowsLoading && !workflowsError && ( -
-
+
+
{t('dashboard.workflow_dropdown.available_workflows')}
{workflows.length === 0 ? ( -
+
{t('dashboard.workflow_dropdown.no_workflows')}
) : ( @@ -299,44 +255,16 @@ function Dashboard () {