workflows page
This commit is contained in:
parent
574e3e1cfa
commit
41a3b8f40e
29 changed files with 1439 additions and 1114 deletions
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<DashboardChatProps> = ({
|
||||
selectedPrompt,
|
||||
workflowState,
|
||||
workflowActions
|
||||
}) => {
|
||||
return (
|
||||
<div className={styles.dashboard_chat}>
|
||||
<DashboardChatArea
|
||||
selectedPrompt={selectedPrompt}
|
||||
workflowState={workflowState}
|
||||
workflowActions={workflowActions}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import { DashboardChatAreaProps } from "./dashboardChatAreaTypes";
|
|||
import styles from './DashboardChatAreaStyles/DashboardChat.module.css';
|
||||
|
||||
const DashboardChatArea: React.FC<DashboardChatAreaProps> = ({
|
||||
selectedPrompt,
|
||||
workflowState,
|
||||
workflowActions
|
||||
}) => {
|
||||
|
|
@ -32,7 +31,6 @@ const DashboardChatArea: React.FC<DashboardChatAreaProps> = ({
|
|||
{/* Bottom Left: Input Area */}
|
||||
<div className={`${styles.quadrant} ${styles.input_quadrant}`}>
|
||||
<InputArea
|
||||
selectedPrompt={selectedPrompt}
|
||||
workflowState={workflowState}
|
||||
workflowActions={workflowActions}
|
||||
attachedFiles={attachedFiles}
|
||||
|
|
|
|||
|
|
@ -1,22 +1,8 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import React from 'react';
|
||||
import { useFileDownload } from '../../../hooks/useWorkflows';
|
||||
import { FileInfo } from './dashboardChatAreaTypes';
|
||||
|
||||
interface AttachedFile {
|
||||
id: number;
|
||||
name: string;
|
||||
size: number;
|
||||
type: string;
|
||||
fileData?: File;
|
||||
objectUrl?: string;
|
||||
}
|
||||
|
||||
interface ConnectedFilesProps {
|
||||
onFileSelect?: (file: FileInfo) => 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<ConnectedFilesProps> = ({
|
||||
onFileSelect,
|
||||
|
|
@ -24,37 +10,16 @@ const ConnectedFiles: React.FC<ConnectedFilesProps> = ({
|
|||
attachedFiles = [],
|
||||
onRemoveFile
|
||||
}) => {
|
||||
const [files, setFiles] = useState<FileInfo[]>([]);
|
||||
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<ConnectedFilesProps> = ({
|
|||
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 (
|
||||
<div style={{ padding: '16px', height: '100%', overflow: 'auto' }}>
|
||||
|
||||
{/* Show attached files count */}
|
||||
<div className={styles.container}>
|
||||
{attachedFiles.length > 0 && (
|
||||
<div style={{
|
||||
marginBottom: '12px',
|
||||
padding: '8px',
|
||||
backgroundColor: '#e3f2fd',
|
||||
borderRadius: '6px',
|
||||
fontSize: '12px',
|
||||
color: '#1976d2',
|
||||
fontWeight: '500'
|
||||
}}>
|
||||
📎 {attachedFiles.length} file{attachedFiles.length !== 1 ? 's' : ''} attached for workflow
|
||||
<div className={styles.attachedInfo}>
|
||||
<IoIosAttach className={styles.attachedInfoIcon}/> {attachedFiles.length} file{attachedFiles.length !== 1 ? 's' : ''} attached for workflow
|
||||
</div>
|
||||
)}
|
||||
|
||||
{allFiles.length === 0 ? (
|
||||
<p style={{ color: 'var(--color-gray)', textAlign: 'center' }}>
|
||||
{convertedFiles.length === 0 ? (
|
||||
<p className={styles.emptyState}>
|
||||
No files connected to this workflow
|
||||
</p>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
||||
{allFiles.map((file) => {
|
||||
const isAttachedFile = attachedFiles.some(af => af.id === file.id);
|
||||
return (
|
||||
<div
|
||||
key={file.id}
|
||||
onClick={() => 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'
|
||||
})
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: '20px' }}>
|
||||
{getFileIcon(file.mimeType)}
|
||||
</span>
|
||||
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{
|
||||
fontWeight: '500',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '6px'
|
||||
}}>
|
||||
{file.name}
|
||||
{isAttachedFile && (
|
||||
<span style={{
|
||||
fontSize: '10px',
|
||||
backgroundColor: '#1976d2',
|
||||
color: 'white',
|
||||
padding: '2px 6px',
|
||||
borderRadius: '10px',
|
||||
fontWeight: 'normal'
|
||||
}}>
|
||||
ATTACHED
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: '12px',
|
||||
color: 'var(--color-gray)'
|
||||
}}>
|
||||
{file.size ? formatFileSize(file.size) : 'Unknown size'}
|
||||
</div>
|
||||
<div className={styles.filesList}>
|
||||
{convertedFiles.map((file) => (
|
||||
<div
|
||||
key={file.id}
|
||||
onClick={() => onFileSelect?.(file)}
|
||||
className={`${styles.fileItem} ${selectedFile?.id === file.id ? styles.selected : ''} ${styles.attached}`}
|
||||
>
|
||||
|
||||
<div className={styles.fileInfo}>
|
||||
<div className={styles.fileName}>
|
||||
{file.name}
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: '4px' }}>
|
||||
{isAttachedFile && onRemoveFile && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onRemoveFile(file.id);
|
||||
}}
|
||||
style={{
|
||||
padding: '4px 8px',
|
||||
fontSize: '12px',
|
||||
backgroundColor: 'transparent',
|
||||
border: '1px solid #ff6b6b',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
color: '#ff6b6b'
|
||||
}}
|
||||
title="Remove from attachment"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
)}
|
||||
<div className={styles.fileSize}>
|
||||
{file.size ? formatFileSize(file.size) : 'Unknown size'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.fileActions}>
|
||||
{onRemoveFile && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDownload(file);
|
||||
onRemoveFile(file.id);
|
||||
}}
|
||||
disabled={isDownloading}
|
||||
style={{
|
||||
padding: '4px 8px',
|
||||
fontSize: '12px',
|
||||
backgroundColor: 'transparent',
|
||||
border: '1px solid var(--color-gray-disabled)',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
title="Download file"
|
||||
className={styles.removeButton}
|
||||
title="Remove from attachment"
|
||||
>
|
||||
⬇️
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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<InputAreaProps> = ({
|
||||
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<InputAreaProps> = ({
|
|||
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<InputAreaProps> = ({
|
|||
}
|
||||
};
|
||||
|
||||
const handlePromptSelect = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
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<InputAreaProps> = ({
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* Show attached files count */}
|
||||
{currentAttachedFiles.length > 0 && (
|
||||
<div className={styles.attached_files_count}>
|
||||
{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 */}
|
||||
<div className={styles.prompt_selection_container}>
|
||||
<div className={styles.prompt_dropdown_wrapper}>
|
||||
<select
|
||||
value={currentSelectedPrompt?.id || ''}
|
||||
onChange={handlePromptSelect}
|
||||
disabled={isSending || shouldShowStopButton || promptsLoading}
|
||||
className={styles.prompt_dropdown}
|
||||
>
|
||||
<option value="">{promptsLoading ? t('chat.input.loading_prompts') : t('chat.input.select_prompt')}</option>
|
||||
{prompts.map(prompt => (
|
||||
<option key={prompt.id} value={prompt.id}>
|
||||
{prompt.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{currentSelectedPrompt && (
|
||||
<button
|
||||
onClick={handleClearPrompt}
|
||||
disabled={isSending || shouldShowStopButton}
|
||||
className={styles.clear_prompt_button}
|
||||
title={t('chat.input.clear_prompt')}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={styles.input_form_container}>
|
||||
<div className={styles.floating_label_textarea}>
|
||||
|
|
@ -251,7 +295,9 @@ const InputArea: React.FC<InputAreaProps> = ({
|
|||
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}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -295,17 +341,11 @@ const InputArea: React.FC<InputAreaProps> = ({
|
|||
{workflowState.currentWorkflowId && !shouldShowStopButton && (
|
||||
<button
|
||||
onClick={() => workflowActions.clearWorkflow()}
|
||||
className={styles.new_chat_button}
|
||||
className={sharedStyles.button_primary}
|
||||
>
|
||||
{t('chat.input.new_chat')}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{selectedPrompt && (
|
||||
<span className={styles.prompt_indicator}>
|
||||
{t('chat.input.using_prompt')} {selectedPrompt.name}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -100,12 +100,6 @@ const LogItem: React.FC<LogItemProps> = ({ log }) => {
|
|||
Source: {log.source}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{log.progress !== undefined && log.progress >= 0 && (
|
||||
<div className={messageStyles.log_progress}>
|
||||
Progress: {log.progress}%
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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<MessageItemProps> = ({ 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 (
|
||||
<div
|
||||
className={`${messageStyles.message_item} ${
|
||||
message.role === 'user' ? messageStyles.user : messageStyles.assistant
|
||||
}`}
|
||||
>
|
||||
<div className={`${messageStyles.message_item} ${isUser ? messageStyles.user : messageStyles.assistant}`}>
|
||||
<div className={messageStyles.message_header}>
|
||||
{message.role === 'user' ? 'You' : message.agentName}
|
||||
{isUser ? 'You' : message.agentName}
|
||||
{message.timestamp && (
|
||||
<span className={messageStyles.message_timestamp_inline}>
|
||||
• {formatTimestamp(message.timestamp)}
|
||||
|
|
@ -163,68 +81,57 @@ const MessageItem: React.FC<MessageItemProps> = ({ message, onFilePreview }) =>
|
|||
)}
|
||||
</div>
|
||||
|
||||
<div className={`${messageStyles.message_bubble} ${
|
||||
message.role === 'user' ? messageStyles.user : messageStyles.assistant
|
||||
}`}>
|
||||
<div className={`${messageStyles.message_bubble} ${isUser ? messageStyles.user : messageStyles.assistant}`}>
|
||||
<div className={messageStyles.message_content}>
|
||||
{message.content || (
|
||||
<span className={messageStyles.message_no_content}>
|
||||
[No message content]
|
||||
</span>
|
||||
)}
|
||||
{message.content || <span className={messageStyles.message_no_content}>[No message content]</span>}
|
||||
</div>
|
||||
|
||||
{hasDocuments && (
|
||||
<div className={`${messageStyles.message_documents} ${
|
||||
message.role === 'user' ? messageStyles.user : messageStyles.assistant
|
||||
}`}>
|
||||
<div className={`${messageStyles.message_documents_header} ${
|
||||
message.role === 'user' ? messageStyles.user : messageStyles.assistant
|
||||
}`}>
|
||||
📎 {message.role === 'user' ? 'Uploaded' : 'Attached'} Files ({message.documents!.length})
|
||||
<div className={`${messageStyles.message_documents} ${isUser ? messageStyles.user : messageStyles.assistant}`}>
|
||||
<div className={`${messageStyles.message_documents_header} ${isUser ? messageStyles.user : messageStyles.assistant}`}>
|
||||
{isUser ? 'Uploaded' : 'Attached'} Files ({message.documents?.length || 0})
|
||||
</div>
|
||||
<div>
|
||||
{message.documents!.map((document, docIndex) => {
|
||||
return (
|
||||
<div
|
||||
key={document.id || docIndex}
|
||||
className={messageStyles.message_document_item}
|
||||
onClick={() => handleDocumentClick(document)}
|
||||
title={`Click to open ${document.name}`}
|
||||
>
|
||||
<span className={messageStyles.message_document_icon}>
|
||||
{getFileIcon(document.type, document.ext)}
|
||||
</span>
|
||||
<div className={messageStyles.message_document_info}>
|
||||
<div className={messageStyles.message_document_name}>
|
||||
{document.ext ? `${document.name}.${document.ext}` : document.name}
|
||||
{message.documents!.map((doc, i) => (
|
||||
<div
|
||||
key={doc.id || i}
|
||||
className={messageStyles.message_document_item}
|
||||
onClick={() => handleDocumentClick(doc)}
|
||||
title={`Click to open ${doc.name}`}
|
||||
>
|
||||
<div className={messageStyles.message_document_info}>
|
||||
<div className={messageStyles.message_document_name}>
|
||||
{doc.ext ? `${doc.name}.${doc.ext}` : doc.name}
|
||||
</div>
|
||||
{doc.size && (
|
||||
<div className={messageStyles.message_document_size}>
|
||||
{formatFileSize(doc.size)}
|
||||
</div>
|
||||
{document.size && (
|
||||
<div className={messageStyles.message_document_size}>
|
||||
{formatFileSize(document.size)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={messageStyles.message_document_actions}>
|
||||
<button
|
||||
onClick={(e) => handlePreview(document, e)}
|
||||
className={messageStyles.message_document_action}
|
||||
title="Preview file"
|
||||
>
|
||||
👁️
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => handleDownload(document, e)}
|
||||
disabled={isDownloading}
|
||||
className={messageStyles.message_document_action}
|
||||
title="Download file"
|
||||
>
|
||||
⬇️
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className={messageStyles.message_document_actions}>
|
||||
<button
|
||||
onClick={(e) => handlePreview(doc, e)}
|
||||
className={messageStyles.message_document_action_button}
|
||||
title="Preview file"
|
||||
>
|
||||
<div className={messageStyles.message_document_action_icon}>
|
||||
<FaEye size={16} />
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => handleDownload(doc, e)}
|
||||
disabled={isDownloading}
|
||||
className={messageStyles.message_document_action_button}
|
||||
title="Download file"
|
||||
>
|
||||
<div className={messageStyles.message_document_action_icon}>
|
||||
<FaDownload size={16} />
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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<MessageListProps> = ({
|
||||
workflowState,
|
||||
onFilePreview
|
||||
}) => {
|
||||
const MessageList: React.FC<MessageListProps> = ({ workflowState, onFilePreview }) => {
|
||||
const { t } = useLanguage();
|
||||
const { request } = useApiRequest();
|
||||
const [transformedMessages, setTransformedMessages] = React.useState<any[]>([]);
|
||||
const [isTransforming, setIsTransforming] = React.useState(false);
|
||||
const scrollContainerRef = React.useRef<HTMLDivElement>(null);
|
||||
const [isUserScrolledUp, setIsUserScrolledUp] = React.useState(false);
|
||||
const lastMessageCountRef = React.useRef(0);
|
||||
const [showProgressBar, setShowProgressBar] = React.useState(false);
|
||||
const [messages, setMessages] = React.useState<any[]>([]);
|
||||
const [isLoading, setIsLoading] = React.useState(false);
|
||||
const scrollRef = React.useRef<HTMLDivElement>(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 (
|
||||
<div className={messageStyles.message_list_container}>
|
||||
<div
|
||||
ref={scrollContainerRef}
|
||||
<div
|
||||
ref={scrollRef}
|
||||
className={isEmpty ? messageStyles.chat_messages_empty : messageStyles.chat_messages}
|
||||
onScroll={checkScrollPosition}
|
||||
>
|
||||
{error && (
|
||||
<div className={messageStyles.message_error}>
|
||||
Error: {error}
|
||||
</div>
|
||||
)}
|
||||
onScroll={checkScroll}
|
||||
>
|
||||
{error && <div className={messageStyles.message_error}>Error: {error}</div>}
|
||||
|
||||
<div className={messageStyles.messages_container}>
|
||||
{timeline.map((timelineItem, index) => {
|
||||
const { type, item } = timelineItem;
|
||||
|
||||
if (type === 'message') {
|
||||
return (
|
||||
<MessageItem
|
||||
key={`message-${item.id}`}
|
||||
message={item}
|
||||
index={index}
|
||||
onFilePreview={onFilePreview}
|
||||
/>
|
||||
);
|
||||
} else if (type === 'log') {
|
||||
return (
|
||||
<LogItem
|
||||
key={`log-${item.id}`}
|
||||
log={item}
|
||||
index={index}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
</div>
|
||||
<div className={messageStyles.messages_container}>
|
||||
{timeline.map((item, index) => {
|
||||
if (item.type === 'message') {
|
||||
return (
|
||||
<MessageItem
|
||||
key={`message-${item.item.id}`}
|
||||
message={item.item}
|
||||
index={index}
|
||||
onFilePreview={onFilePreview}
|
||||
/>
|
||||
);
|
||||
} else if (item.type === 'log') {
|
||||
return (
|
||||
<LogItem
|
||||
key={`log-${item.item.id}`}
|
||||
log={item.item}
|
||||
index={index}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
</div>
|
||||
|
||||
{isEmpty && (
|
||||
<div className={messageStyles.message_empty_state}>
|
||||
<div className={messageStyles.message_empty_state_icon}>
|
||||
<IoIosChatbubbles />
|
||||
<div className={messageStyles.message_empty_state}>
|
||||
<div className={messageStyles.message_empty_state_icon}>
|
||||
<IoIosChatbubbles />
|
||||
</div>
|
||||
<h3>{t('chat.messages.no_workflow_selected')}</h3>
|
||||
<p>{t('chat.messages.no_workflow_selected_description')}</p>
|
||||
</div>
|
||||
<h3>{t('chat.messages.no_workflow_selected')}</h3>
|
||||
<p>{t('chat.messages.no_workflow_selected_description')}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Progress Bar */}
|
||||
{showProgressBar && (
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showProgress && (
|
||||
<div className={messageStyles.workflow_progress_container}>
|
||||
<div className={messageStyles.workflow_progress_label}>
|
||||
<span>{t('chat.messages.workflow_progress')}</span>
|
||||
<div className={messageStyles.workflow_progress_label}>
|
||||
<span>{t('chat.messages.workflow_progress')}</span>
|
||||
<span>{progressText}</span>
|
||||
</div>
|
||||
<div className={messageStyles.workflow_progress_bar}>
|
||||
<div
|
||||
className={messageStyles.workflow_progress_fill}
|
||||
style={{ width: `${progressPercentage}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={messageStyles.workflow_progress_bar}>
|
||||
<div
|
||||
className={messageStyles.workflow_progress_fill}
|
||||
style={{ width: `${progressPercent}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
<button
|
||||
className={`${messageStyles.scroll_to_bottom_btn} ${
|
||||
(isUserScrolledUp && timeline.length > 0) ? messageStyles.visible : messageStyles.hidden
|
||||
(isScrolledUp && timeline.length > 0) ? messageStyles.visible : messageStyles.hidden
|
||||
}`}
|
||||
onClick={scrollToBottom}
|
||||
title={t('chat.messages.scroll_to_bottom_btn')}
|
||||
>
|
||||
<IoIosArrowDown className={messageStyles.scroll_to_bottom_btn_icon} />
|
||||
</button>
|
||||
onClick={scrollToBottom}
|
||||
title={t('chat.messages.scroll_to_bottom_btn')}
|
||||
>
|
||||
<IoIosArrowDown className={messageStyles.scroll_to_bottom_btn_icon} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'><path fill='none' stroke='%23666' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' d='M5 7l3 3 3-3'/></svg>"); 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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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<number> } } = {};
|
||||
const taskCounts: { [key: number]: { total: number; completed: Set<number> } } = {};
|
||||
|
||||
// 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<any> => {
|
||||
let documents: any[] = [];
|
||||
export const transformWorkflowMessage = async (msg: any, request: any): Promise<any> => {
|
||||
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
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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<boolean>;
|
||||
stopWorkflow: () => Promise<boolean>;
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ export function useWorkflowManager(initialWorkflowId?: string | null): [Workflow
|
|||
const [isPolling, setIsPolling] = useState(false);
|
||||
const [pendingMessages, setPendingMessages] = useState<any[]>([]);
|
||||
const [sentUserMessages, setSentUserMessages] = useState<Set<string>>(new Set()); // Track sent user messages
|
||||
const [selectedPrompt, setSelectedPrompt] = useState<any | null>(null); // Selected prompt state
|
||||
const pollingIntervalRef = useRef<number | null>(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];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
|||
<FormGenerator
|
||||
data={files}
|
||||
columns={columns}
|
||||
title={t('files.table.title')}
|
||||
loading={loading}
|
||||
searchable={true}
|
||||
filterable={true}
|
||||
|
|
@ -49,8 +99,9 @@ export function DateienTable({ className = '' }: DateienTableProps) {
|
|||
resizable={true}
|
||||
pagination={true}
|
||||
pageSize={10}
|
||||
selectable={false}
|
||||
onRowClick={undefined}
|
||||
onDelete={handleDeleteSingle}
|
||||
onDeleteMultiple={handleDeleteMultiple}
|
||||
actions={actions}
|
||||
className={styles.dateienFormGenerator}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -38,13 +38,14 @@ export interface FormGeneratorProps<T = any> {
|
|||
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<T extends Record<string, any>>({
|
||||
data,
|
||||
columns: providedColumns,
|
||||
title,
|
||||
searchable = true,
|
||||
filterable = true,
|
||||
sortable = true,
|
||||
|
|
@ -55,9 +56,11 @@ export function FormGenerator<T extends Record<string, any>>({
|
|||
showPageSizeSelector = true,
|
||||
onRowClick,
|
||||
onRowSelect,
|
||||
selectable = false,
|
||||
selectable = true, // Default to true for selection functionality
|
||||
loading = false,
|
||||
actions = [],
|
||||
onDelete,
|
||||
onDeleteMultiple,
|
||||
className = ''
|
||||
}: FormGeneratorProps<T>) {
|
||||
const { t } = useLanguage();
|
||||
|
|
@ -116,17 +119,12 @@ export function FormGenerator<T extends Record<string, any>>({
|
|||
useEffect(() => {
|
||||
const initialWidths: Record<string, number> = {};
|
||||
|
||||
// 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<T extends Record<string, any>>({
|
|||
}
|
||||
};
|
||||
|
||||
// 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<T extends Record<string, any>>({
|
|||
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<T extends Record<string, any>>({
|
|||
return (
|
||||
<div className={`${styles.formGenerator} ${className}`}>
|
||||
|
||||
{(searchable || filterable) && (
|
||||
{(searchable || filterable || (selectable && selectedRows.size > 0)) && (
|
||||
<div className={styles.controls}>
|
||||
{searchable && (
|
||||
{/* Delete Controls - Show when items are selected */}
|
||||
{selectable && selectedRows.size > 0 && (
|
||||
<div className={styles.deleteControlsIntegrated}>
|
||||
{selectedRows.size === 1 && onDelete && (
|
||||
<button
|
||||
onClick={() => {
|
||||
const selectedIndex = Array.from(selectedRows)[0];
|
||||
const selectedRow = paginatedData[selectedIndex];
|
||||
handleDeleteSingle(selectedRow, selectedIndex);
|
||||
}}
|
||||
className={styles.deleteButton}
|
||||
title={t('formgen.delete.single', 'Delete selected item')}
|
||||
>
|
||||
<span className={styles.deleteIcon}>✕</span>
|
||||
{t('formgen.delete.single', 'Delete')}
|
||||
</button>
|
||||
)}
|
||||
{selectedRows.size > 1 && onDeleteMultiple && (
|
||||
<button
|
||||
onClick={handleDeleteMultiple}
|
||||
className={styles.deleteAllButton}
|
||||
title={t('formgen.delete.multiple', `Delete ${selectedRows.size} selected items`)}
|
||||
>
|
||||
<span className={styles.deleteIcon}>✕</span>
|
||||
{t('formgen.delete.multiple', `Delete All (${selectedRows.size})`)}
|
||||
</button>
|
||||
)}
|
||||
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Search Controls - Hide when items are selected */}
|
||||
{searchable && selectedRows.size === 0 && (
|
||||
<div className={styles.searchContainer}>
|
||||
<div className={styles.floatingLabelInput}>
|
||||
<input
|
||||
|
|
@ -523,33 +581,24 @@ export function FormGenerator<T extends Record<string, any>>({
|
|||
<table ref={tableRef} className={styles.table}>
|
||||
<thead>
|
||||
<tr>
|
||||
{actions.length > 0 && (
|
||||
<th
|
||||
className={`${styles.actionsColumn} ${styles.th} ${resizable ? styles.sortable : ''}`}
|
||||
style={{
|
||||
width: columnWidths['actions'] || 120,
|
||||
minWidth: columnWidths['actions'] || 120,
|
||||
maxWidth: columnWidths['actions'] || 120
|
||||
}}
|
||||
>
|
||||
|
||||
{resizable && (
|
||||
<div
|
||||
className={styles.resizeHandle}
|
||||
onMouseDown={(e) => handleMouseDown(e, 'actions')}
|
||||
/>
|
||||
)}
|
||||
</th>
|
||||
)}
|
||||
{selectable && (
|
||||
<th className={styles.selectColumn} style={{ width: '40px', minWidth: '40px', maxWidth: '40px' }}>
|
||||
<th className={styles.selectColumn} style={{ width: '50px', minWidth: '50px', maxWidth: '50px' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedRows.size === paginatedData.length && paginatedData.length > 0}
|
||||
onChange={handleSelectAll}
|
||||
title="Select all items"
|
||||
/>
|
||||
</th>
|
||||
)}
|
||||
{actions.length > 0 && (
|
||||
<th
|
||||
className={styles.actionsColumn}
|
||||
style={{ width: '120px', minWidth: '120px', maxWidth: '120px' }}
|
||||
>
|
||||
|
||||
</th>
|
||||
)}
|
||||
{detectedColumns.map(column => (
|
||||
<th
|
||||
key={column.key}
|
||||
|
|
@ -588,14 +637,21 @@ export function FormGenerator<T extends Record<string, any>>({
|
|||
className={`${styles.tr} ${selectedRows.has(index) ? styles.selected : ''} ${onRowClick ? styles.clickable : ''}`}
|
||||
onClick={() => onRowClick?.(row, index)}
|
||||
>
|
||||
{actions.length > 0 && (
|
||||
{selectable && (
|
||||
<td className={styles.selectColumn} style={{ width: '50px', minWidth: '50px', maxWidth: '50px' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedRows.has(index)}
|
||||
onChange={() => handleRowSelect(index)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
title="Select this item"
|
||||
/>
|
||||
</td>
|
||||
)}
|
||||
{actions.length > 0 && (
|
||||
<td
|
||||
className={styles.actionsColumn}
|
||||
style={{
|
||||
width: columnWidths['actions'] || 120,
|
||||
minWidth: columnWidths['actions'] || 120,
|
||||
maxWidth: columnWidths['actions'] || 120
|
||||
}}
|
||||
style={{ width: '120px', minWidth: '120px', maxWidth: '120px' }}
|
||||
>
|
||||
<div className={styles.actionButtons}>
|
||||
{actions.map((action, actionIndex) => (
|
||||
|
|
@ -618,16 +674,6 @@ export function FormGenerator<T extends Record<string, any>>({
|
|||
</div>
|
||||
</td>
|
||||
)}
|
||||
{selectable && (
|
||||
<td className={styles.selectColumn} style={{ width: '40px', minWidth: '40px', maxWidth: '40px' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedRows.has(index)}
|
||||
onChange={() => handleRowSelect(index)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
</td>
|
||||
)}
|
||||
{detectedColumns.map(column => (
|
||||
<td
|
||||
key={column.key}
|
||||
|
|
|
|||
|
|
@ -143,7 +143,7 @@
|
|||
}
|
||||
|
||||
.workflowName {
|
||||
color: #f8f9fa;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.status-running {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,14 @@
|
|||
import { useMemo } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { IoIosTrash, IoIosPlay } from 'react-icons/io';
|
||||
import { MdModeEdit } from 'react-icons/md';
|
||||
|
||||
import { FormGenerator, ColumnConfig } from '../FormGenerator/FormGenerator';
|
||||
import { useWorkflows, useWorkflowOperations, Workflow } from '../../hooks/useWorkflows';
|
||||
import { useLanguage } from '../../contexts/LanguageContext';
|
||||
import { Popup, EditForm } from '../Popup';
|
||||
import type { EditFieldConfig } from '../Popup/EditForm';
|
||||
import styles from './WorkflowsTable.module.css';
|
||||
|
||||
interface WorkflowsTableProps {
|
||||
|
|
@ -10,14 +17,41 @@ interface WorkflowsTableProps {
|
|||
|
||||
function WorkflowsTable({ className = '' }: WorkflowsTableProps) {
|
||||
const { workflows, loading, error, refetch } = useWorkflows();
|
||||
const navigate = useNavigate();
|
||||
|
||||
// Debug: Log workflow data to see the actual structure
|
||||
console.log('Workflows data:', workflows);
|
||||
const {
|
||||
stopWorkflow,
|
||||
deleteWorkflow,
|
||||
stoppingWorkflows,
|
||||
deleteWorkflow,
|
||||
updateWorkflow,
|
||||
deletingWorkflows
|
||||
} = useWorkflowOperations();
|
||||
const { t } = useLanguage();
|
||||
|
||||
// Edit modal state
|
||||
const [editModalOpen, setEditModalOpen] = useState(false);
|
||||
const [editingWorkflow, setEditingWorkflow] = useState<Workflow | null>(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) => (
|
||||
<span className={styles.workflowName}>
|
||||
{value || row.title || t('workflows.unnamed')}
|
||||
{value || t('workflows.unnamed')}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
|
|
@ -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 <IoIosPlay />;
|
||||
},
|
||||
onClick: (row: Workflow) => {
|
||||
if (row.status === 'running' && !stoppingWorkflows.has(row.id)) {
|
||||
handleStopWorkflow(row);
|
||||
}
|
||||
handlePlayWorkflow(row);
|
||||
}
|
||||
},
|
||||
{
|
||||
label: t('workflows.action.edit'),
|
||||
icon: (_row: Workflow) => {
|
||||
return <MdModeEdit />;
|
||||
},
|
||||
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 <IoIosTrash />;
|
||||
},
|
||||
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) {
|
|||
<FormGenerator
|
||||
data={workflows}
|
||||
columns={columns}
|
||||
title={t('workflows.table.title')}
|
||||
searchable={true}
|
||||
filterable={true}
|
||||
sortable={true}
|
||||
|
|
@ -214,12 +332,33 @@ function WorkflowsTable({ className = '' }: WorkflowsTableProps) {
|
|||
pageSize={10}
|
||||
loading={loading}
|
||||
actions={actions}
|
||||
onDelete={handleDeleteSingle}
|
||||
onDeleteMultiple={handleDeleteMultiple}
|
||||
className={styles.workflowsFormGenerator}
|
||||
onRowClick={(workflow: Workflow) => {
|
||||
// TODO: Navigate to workflow detail view
|
||||
console.log('Clicked workflow:', workflow);
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Edit Workflow Modal */}
|
||||
<Popup
|
||||
isOpen={editModalOpen}
|
||||
title={t('workflows.edit.title', 'Edit Workflow')}
|
||||
onClose={handleCancelEdit}
|
||||
size="small"
|
||||
>
|
||||
{editingWorkflow && (
|
||||
<EditForm
|
||||
data={editingWorkflow}
|
||||
fields={editWorkflowFields}
|
||||
onSave={handleSaveWorkflow}
|
||||
onCancel={handleCancelEdit}
|
||||
saveButtonText={t('common.save', 'Save')}
|
||||
cancelButtonText={t('common.cancel', 'Cancel')}
|
||||
/>
|
||||
)}
|
||||
</Popup>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Prompt[]>([]);
|
||||
const { request, isLoading: loading, error } = useApiRequest<null, Prompt[]>();
|
||||
|
||||
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<Prompt[]>([]);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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...',
|
||||
|
|
|
|||
|
|
@ -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...',
|
||||
|
|
|
|||
|
|
@ -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...',
|
||||
|
|
|
|||
|
|
@ -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<Prompt | null>(null);
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||
const dropdownRef = useRef<HTMLDivElement>(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 (
|
||||
<div className={sharedStyles.pageContainer}>
|
||||
<div className={`${sharedStyles.pageCard} ${styles.dashboardPageCard}`}>
|
||||
{/* Vertical Divider - spans from after title to bottom */}
|
||||
<div className={styles.verticalDivider}></div>
|
||||
|
||||
<div className={sharedStyles.pageHeader}>
|
||||
<h1 className={sharedStyles.pageTitle}>{t('nav.dashboard')}</h1>
|
||||
<div style={{ display: 'flex', gap: '15px', alignItems: 'center' }}>
|
||||
<div className={styles.headerControls}>
|
||||
{workflowState.currentWorkflowId ? (
|
||||
<>
|
||||
{(() => {
|
||||
const stats = getWorkflowStats();
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '20px',
|
||||
padding: '12px 20px',
|
||||
backgroundColor: 'var(--color-gray-disabled)',
|
||||
borderRadius: '12px',
|
||||
fontSize: '13px',
|
||||
fontWeight: '500'
|
||||
}}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '2px' }}>
|
||||
<span style={{ color: 'var(--color-text-secondary)', fontSize: '11px', textTransform: 'uppercase' }}>{t('dashboard.stats.workflow')}</span>
|
||||
<span style={{ color: 'var(--color-text)', fontFamily: 'monospace' }}>{displayWorkflowId}</span>
|
||||
<div className={styles.workflowStats}>
|
||||
<div className={styles.statItem}>
|
||||
<span className={styles.statLabel}>{t('dashboard.stats.workflow')}</span>
|
||||
<span className={styles.statValueMonospace}>{displayWorkflowId}</span>
|
||||
</div>
|
||||
|
||||
{stats && (
|
||||
<>
|
||||
<div className={styles.statDivider}></div>
|
||||
|
||||
<div className={styles.statItem}>
|
||||
<span className={styles.statLabel}>{t('dashboard.stats.status')}</span>
|
||||
<span className={`${styles.statValue} ${styles.statValueBold}`} style={{ color: getStatusColor(stats.status) }}>
|
||||
{formatStatus(stats.status)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{stats && (
|
||||
<>
|
||||
<div style={{ width: '1px', height: '40px', backgroundColor: 'var(--color-primary)', opacity: 0.3 }}></div>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '2px' }}>
|
||||
<span style={{ color: 'var(--color-text-secondary)', fontSize: '11px', textTransform: 'uppercase' }}>{t('dashboard.stats.status')}</span>
|
||||
<span style={{ color: getStatusColor(stats.status), fontWeight: '600' }}>{formatStatus(stats.status)}</span>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '2px' }}>
|
||||
<span style={{ color: 'var(--color-text-secondary)', fontSize: '11px', textTransform: 'uppercase' }}>{t('dashboard.stats.rounds')}</span>
|
||||
<span style={{ color: 'var(--color-text)' }}>{stats.rounds}</span>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '2px' }}>
|
||||
<span style={{ color: 'var(--color-text-secondary)', fontSize: '11px', textTransform: 'uppercase' }}>{t('dashboard.stats.messages')}</span>
|
||||
<span style={{ color: 'var(--color-text)' }}>{stats.messageCount}</span>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '2px' }}>
|
||||
<span style={{ color: 'var(--color-text-secondary)', fontSize: '11px', textTransform: 'uppercase' }}>{t('dashboard.stats.files')}</span>
|
||||
<span style={{ color: 'var(--color-text)' }}>{stats.fileCount}</span>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '2px' }}>
|
||||
<span style={{ color: 'var(--color-text-secondary)', fontSize: '11px', textTransform: 'uppercase' }}>{t('dashboard.stats.tokens')}</span>
|
||||
<span style={{ color: 'var(--color-text)' }}>{stats.tokenCount.toLocaleString()}</span>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '2px' }}>
|
||||
<span style={{ color: 'var(--color-text-secondary)', fontSize: '11px', textTransform: 'uppercase' }}>{t('dashboard.stats.data_sent')}</span>
|
||||
<span style={{ color: 'var(--color-text)' }}>{formatBytes(stats.bytesSent)}</span>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '2px' }}>
|
||||
<span style={{ color: 'var(--color-text-secondary)', fontSize: '11px', textTransform: 'uppercase' }}>{t('dashboard.stats.data_received')}</span>
|
||||
<span style={{ color: 'var(--color-text)' }}>{formatBytes(stats.bytesReceived)}</span>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '2px' }}>
|
||||
<span style={{ color: 'var(--color-text-secondary)', fontSize: '11px', textTransform: 'uppercase' }}>{t('dashboard.stats.success_rate')}</span>
|
||||
<span style={{
|
||||
color: stats.successRate >= 90 ? 'var(--color-success)' :
|
||||
stats.successRate >= 70 ? 'var(--color-warning)' : 'var(--color-error)'
|
||||
}}>{stats.successRate}%</span>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '2px' }}>
|
||||
<span style={{ color: 'var(--color-text-secondary)', fontSize: '11px', textTransform: 'uppercase' }}>{t('dashboard.stats.errors')}</span>
|
||||
<span style={{ color: stats.errorCount > 0 ? 'var(--color-error)' : 'var(--color-text)' }}>{stats.errorCount}</span>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '2px' }}>
|
||||
<span style={{ color: 'var(--color-text-secondary)', fontSize: '11px', textTransform: 'uppercase' }}>{t('dashboard.stats.started')}</span>
|
||||
<span style={{ color: 'var(--color-text)', fontSize: '12px' }}>{formatDate(stats.startedAt)}</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
<div className={styles.statItem}>
|
||||
<span className={styles.statLabel}>{t('dashboard.stats.rounds')}</span>
|
||||
<span className={styles.statValue}>{stats.rounds}</span>
|
||||
</div>
|
||||
|
||||
<div className={styles.statItem}>
|
||||
<span className={styles.statLabel}>{t('dashboard.stats.messages')}</span>
|
||||
<span className={styles.statValue}>{stats.messageCount}</span>
|
||||
</div>
|
||||
|
||||
<div className={styles.statItem}>
|
||||
<span className={styles.statLabel}>{t('dashboard.stats.files')}</span>
|
||||
<span className={styles.statValue}>{stats.fileCount}</span>
|
||||
</div>
|
||||
|
||||
<div className={styles.statItem}>
|
||||
<span className={styles.statLabel}>{t('dashboard.stats.tokens')}</span>
|
||||
<span className={styles.statValue}>{stats.tokenCount.toLocaleString()}</span>
|
||||
</div>
|
||||
|
||||
<div className={styles.statItem}>
|
||||
<span className={styles.statLabel}>{t('dashboard.stats.data_sent')}</span>
|
||||
<span className={styles.statValue}>{formatBytes(stats.bytesSent)}</span>
|
||||
</div>
|
||||
|
||||
<div className={styles.statItem}>
|
||||
<span className={styles.statLabel}>{t('dashboard.stats.data_received')}</span>
|
||||
<span className={styles.statValue}>{formatBytes(stats.bytesReceived)}</span>
|
||||
</div>
|
||||
|
||||
<div className={styles.statItem}>
|
||||
<span className={styles.statLabel}>{t('dashboard.stats.success_rate')}</span>
|
||||
<span className={styles.statValue} style={{
|
||||
color: stats.successRate >= 90 ? 'var(--color-success)' :
|
||||
stats.successRate >= 70 ? 'var(--color-warning)' : 'var(--color-error)'
|
||||
}}>{stats.successRate}%</span>
|
||||
</div>
|
||||
|
||||
<div className={styles.statItem}>
|
||||
<span className={styles.statLabel}>{t('dashboard.stats.errors')}</span>
|
||||
<span className={`${styles.statValue} ${stats.errorCount > 0 ? styles.statValueError : ''}`}>
|
||||
{stats.errorCount}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className={styles.statItem}>
|
||||
<span className={styles.statLabel}>{t('dashboard.stats.started')}</span>
|
||||
<span className={`${styles.statValue} ${styles.statValueSmall}`}>
|
||||
{formatDate(stats.startedAt)}
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
className={sharedStyles.secondaryButton}
|
||||
className={sharedStyles.primaryButton}
|
||||
onClick={handleResetWorkflow}
|
||||
aria-label="Reset workflow"
|
||||
>
|
||||
<span className={sharedStyles.buttonIcon}><IoMdRefresh /></span>
|
||||
Reset
|
||||
<span className={sharedStyles.buttonIcon}><IoMdClose /></span>
|
||||
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<div style={{ position: 'relative', display: 'inline-block' }} ref={dropdownRef}>
|
||||
<div className={styles.dropdownContainer} ref={dropdownRef}>
|
||||
<button
|
||||
className={sharedStyles.secondaryButton}
|
||||
className={`${sharedStyles.primaryButton} ${styles.dropdownButton}`}
|
||||
onClick={() => setIsDropdownOpen(!isDropdownOpen)}
|
||||
disabled={workflowsLoading}
|
||||
aria-expanded={isDropdownOpen}
|
||||
aria-haspopup="listbox"
|
||||
style={{ minWidth: '180px', justifyContent: 'space-between' }}
|
||||
>
|
||||
<span>
|
||||
{workflowsLoading
|
||||
|
|
@ -249,49 +234,20 @@ function Dashboard () {
|
|||
}
|
||||
</span>
|
||||
<IoMdArrowDropdown
|
||||
className={sharedStyles.buttonIcon}
|
||||
style={{
|
||||
transform: isDropdownOpen ? 'rotate(180deg)' : 'rotate(0deg)',
|
||||
transition: 'transform 0.2s ease'
|
||||
}}
|
||||
className={`${sharedStyles.buttonIcon} ${styles.dropdownIcon} ${
|
||||
isDropdownOpen ? styles.dropdownIconOpen : ''
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
|
||||
{isDropdownOpen && !workflowsLoading && !workflowsError && (
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: '100%',
|
||||
left: 0,
|
||||
right: 0,
|
||||
marginTop: '4px',
|
||||
backgroundColor: 'var(--color-bg)',
|
||||
border: '1px solid var(--color-primary)',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
|
||||
zIndex: 1000,
|
||||
maxHeight: '300px',
|
||||
overflowY: 'auto'
|
||||
}}>
|
||||
<div style={{
|
||||
padding: '12px 16px',
|
||||
fontSize: '12px',
|
||||
fontWeight: 600,
|
||||
color: 'var(--color-text)',
|
||||
backgroundColor: 'var(--color-gray-disabled)',
|
||||
borderBottom: '1px solid var(--color-primary)',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.5px'
|
||||
}}>
|
||||
<div className={styles.dropdownMenu}>
|
||||
<div className={styles.dropdownHeader}>
|
||||
{t('dashboard.workflow_dropdown.available_workflows')}
|
||||
</div>
|
||||
|
||||
{workflows.length === 0 ? (
|
||||
<div style={{
|
||||
padding: '12px 16px',
|
||||
fontSize: '14px',
|
||||
color: 'var(--color-text-secondary)',
|
||||
fontStyle: 'italic'
|
||||
}}>
|
||||
<div className={styles.dropdownEmpty}>
|
||||
{t('dashboard.workflow_dropdown.no_workflows')}
|
||||
</div>
|
||||
) : (
|
||||
|
|
@ -299,44 +255,16 @@ function Dashboard () {
|
|||
<button
|
||||
key={workflow.id}
|
||||
onClick={() => handleWorkflowSelect(workflow.id)}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '12px 16px',
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
textAlign: 'left',
|
||||
cursor: 'pointer',
|
||||
fontFamily: 'var(--font-family)',
|
||||
transition: 'background-color 0.2s ease',
|
||||
borderBottom: '1px solid var(--color-gray-disabled)'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.backgroundColor = 'var(--color-gray-disabled)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.backgroundColor = 'transparent';
|
||||
}}
|
||||
className={styles.dropdownItem}
|
||||
>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
|
||||
<span style={{
|
||||
fontSize: '14px',
|
||||
fontWeight: 500,
|
||||
color: 'var(--color-text)'
|
||||
}}>
|
||||
<div className={styles.workflowInfo}>
|
||||
<span className={styles.workflowName}>
|
||||
{getWorkflowDisplayName(workflow)}
|
||||
</span>
|
||||
<span style={{
|
||||
fontSize: '12px',
|
||||
color: 'var(--color-text-secondary)',
|
||||
fontFamily: 'monospace'
|
||||
}}>
|
||||
<span className={styles.workflowId}>
|
||||
ID: {formatWorkflowId(workflow.id)}
|
||||
</span>
|
||||
<span style={{
|
||||
fontSize: '12px',
|
||||
color: 'var(--color-text-secondary)',
|
||||
textTransform: 'capitalize'
|
||||
}}>
|
||||
<span className={styles.workflowStatus}>
|
||||
{workflow.status}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -355,7 +283,6 @@ function Dashboard () {
|
|||
<div className={styles.chatLogContainer}>
|
||||
<div className={styles.chatArea}>
|
||||
<DashboardChat
|
||||
selectedPrompt={selectedPrompt}
|
||||
workflowState={workflowState}
|
||||
workflowActions={workflowActions}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ function Dateien() {
|
|||
onClick={triggerFilePicker}
|
||||
>
|
||||
<span className={styles.dropZoneText}>
|
||||
{isDragOver ? 'Drop files here' : 'Drop files here or click to browse'}
|
||||
{isDragOver ? 'Drop files here' : 'Drop files here'}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -22,14 +22,13 @@
|
|||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Dashboard-specific styles */
|
||||
.dashboardPageCard {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.verticalDivider {
|
||||
position: absolute;
|
||||
top: calc(86px + 23px + 1px); /* pageHeader height + gap + horizontalDivider height */
|
||||
top: calc(86px + 23px + 1px);
|
||||
bottom: 0;
|
||||
left: calc(66.666% - 9px);
|
||||
width: 1px;
|
||||
|
|
@ -38,3 +37,159 @@
|
|||
pointer-events: none;
|
||||
}
|
||||
|
||||
.headerControls {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.workflowStats {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
padding: 12px 20px;
|
||||
border-radius: 12px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.statItem {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.statLabel {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.statValue {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.statValueMonospace {
|
||||
color: var(--color-text);
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.statDivider {
|
||||
width: 1px;
|
||||
height: 40px;
|
||||
background-color: var(--color-primary);
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.dropdownContainer {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.dropdownButton {
|
||||
min-width: 180px;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.dropdownIcon {
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.dropdownIconOpen {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.dropdownMenu {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin-top: 4px;
|
||||
background-color: var(--color-bg);
|
||||
border: 1px solid var(--color-primary);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
z-index: 1000;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.dropdownHeader {
|
||||
padding: 12px 16px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
background-color: var(--color-bg);
|
||||
border-bottom: 1px solid var(--color-primary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.dropdownEmpty {
|
||||
padding: 12px 16px;
|
||||
font-size: 14px;
|
||||
color: var(--color-text);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.dropdownItem {
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
background: none;
|
||||
border: none;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
font-family: var(--font-family);
|
||||
transition: 0.2s ease;
|
||||
border-bottom: 1px solid var(--color-gray-disabled);
|
||||
}
|
||||
|
||||
.dropdownItem:hover {
|
||||
background-color: var(--color-secondary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.workflowInfo {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.workflowName {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.workflowId {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-secondary);
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.workflowStatus {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-secondary);
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.statValueSuccess {
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.statValueWarning {
|
||||
color: var(--color-warning);
|
||||
}
|
||||
|
||||
.statValueError {
|
||||
color: var(--color-error);
|
||||
}
|
||||
|
||||
.statValueBold {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.statValueSmall {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -96,19 +96,18 @@
|
|||
background: var(--color-bg);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
min-width: 200px;
|
||||
min-width: 400px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.dropZone:hover {
|
||||
border-color: var(--color-secondary);
|
||||
background: var(--color-gray-disabled);
|
||||
}
|
||||
|
||||
.dropZoneActive {
|
||||
border-color: var(--color-secondary);
|
||||
background: var(--color-gray-disabled);
|
||||
border-style: solid;
|
||||
min-width: 400px;
|
||||
}
|
||||
|
||||
.dropZoneText {
|
||||
|
|
|
|||
Loading…
Reference in a new issue