added page activator and cacher

This commit is contained in:
Ida Dittrich 2025-08-20 07:44:55 +02:00
parent c5ecd88e66
commit 22d582f72b
55 changed files with 2094 additions and 4048 deletions

View file

@ -11,15 +11,8 @@ import { AuthProvider } from './auth/authProvider';
import { ProtectedRoute } from './auth/ProtectedRoute';
import { LanguageProvider } from './contexts/LanguageContext';
import Home from './pages/Home/Home';
import Dateien from './pages/Home/Dateien';
import TeamBereich from './pages/Home/TeamBereich';
import Dashboard from './pages/Home/Dashboard';
import Einstellungen from './pages/Home/Einstellungen';
// Import the global light theme CSS variables as default
import './assets/styles/light.css';
import Connections from './pages/Home/Connections';
import Workflows from './pages/Home/Workflows';
import TestSharepoint from './pages/Home/TestSharepoint';
function App() {
// Load saved theme preference on app mount
@ -49,14 +42,8 @@ function App() {
<Home />
</ProtectedRoute>
}>
<Route index element={<Dashboard />} />
<Route path="dashboard" element={<Dashboard />} />
<Route path="dateien" element={<Dateien />} />
<Route path="team-bereich" element={<TeamBereich />} />
<Route path="connections" element={<Connections />} />
<Route path="workflows" element={<Workflows />} />
<Route path="einstellungen" element={<Einstellungen />} />
<Route path="testSharepoint" element={<TestSharepoint />} />
{/* All page routing is now handled by the Page Loader in Home.tsx */}
<Route path="*" element={null} />
</Route>
</Routes>
</Router>

View file

@ -24,13 +24,10 @@ const DashboardChat: React.FC<DashboardChatProps> = ({
onWorkflowResume
}) => {
const { t } = useLanguage();
const [activeTab, setActiveTab] = useState(t('dashboard.chat.area'));
const [resumeWorkflowId, setResumeWorkflowId] = useState<string | null>(null);
const handleWorkflowResume = (workflowId: string) => {
// Switch to Chat Area tab first
setActiveTab(t('dashboard.chat.area'));
// Set the workflow ID to resume
setResumeWorkflowId(workflowId);
// Then call the parent's resume handler
if (onWorkflowResume) {

View file

@ -3,151 +3,105 @@ import MessageList from "./DashboardChatAreaMessageList";
import FilePreview from "./DashboardChatAreaFilePreview";
import InputArea from "./DashboardChatAreaInput";
import ConnectedFiles from "./DashboardChatAreaConnectedFiles";
import "./DashboardChatAreaStyles/grid.css";
import { DashboardChatAreaProps } from "./dashboardChatAreaTypes";
import { useWorkflowManager } from "./useWorkflowManager";
import styles from './DashboardChatAreaStyles/DashboardChat.module.css';
const DashboardChatArea: React.FC<DashboardChatAreaProps> = ({
selectedPrompt,
onPromptUsed,
onWorkflowIdChange,
onWorkflowCompletedChange,
resumeWorkflowId
selectedPrompt,
onPromptUsed,
onWorkflowIdChange,
onWorkflowCompletedChange,
resumeWorkflowId
}) => {
// Grid sizing state
const [horizontalSplit, setHorizontalSplit] = useState(60); // percentage
const [verticalSplit, setVerticalSplit] = useState(60); // percentage
const [isDragging, setIsDragging] = useState<'horizontal' | 'vertical' | null>(null);
// Fixed grid layout - no resizing
// File selection state
const [selectedFile, setSelectedFile] = useState<any>(null);
const [attachedFiles, setAttachedFiles] = useState<any[]>([]);
// Workflow state
const [currentWorkflowId, setCurrentWorkflowId] = useState<string | null>(resumeWorkflowId || null);
// File selection state
const [selectedFile, setSelectedFile] = useState<any>(null);
const [attachedFiles, setAttachedFiles] = useState<any[]>([]);
// Centralized workflow management
const [workflowState, workflowActions] = useWorkflowManager(resumeWorkflowId);
// Update current workflow ID when resumeWorkflowId changes
React.useEffect(() => {
if (resumeWorkflowId !== currentWorkflowId) {
setCurrentWorkflowId(resumeWorkflowId);
}
}, [resumeWorkflowId, currentWorkflowId]);
// Notify parent when workflow ID changes
React.useEffect(() => {
if (onWorkflowIdChange && workflowState.currentWorkflowId !== resumeWorkflowId) {
onWorkflowIdChange(workflowState.currentWorkflowId);
}
}, [workflowState.currentWorkflowId, onWorkflowIdChange, resumeWorkflowId]);
// Handle workflow ID changes
const handleWorkflowIdChange = React.useCallback((workflowId: string | null) => {
setCurrentWorkflowId(workflowId);
if (onWorkflowIdChange) {
onWorkflowIdChange(workflowId);
}
}, [onWorkflowIdChange]);
// Notify parent when workflow is completed
React.useEffect(() => {
if (onWorkflowCompletedChange && workflowState.workflow) {
const isCompleted = ['completed', 'failed', 'stopped'].includes(workflowState.workflow.status);
onWorkflowCompletedChange(isCompleted);
}
}, [workflowState.workflow?.status, onWorkflowCompletedChange]);
// Handle resizing
const handleMouseDown = (direction: 'horizontal' | 'vertical') => (e: React.MouseEvent) => {
e.preventDefault();
setIsDragging(direction);
};
// Auto-load workflow when resumeWorkflowId changes externally
React.useEffect(() => {
if (resumeWorkflowId && resumeWorkflowId !== workflowState.currentWorkflowId) {
console.log(`🔄 Loading workflow from external prop: ${resumeWorkflowId}`);
workflowActions.loadWorkflow(resumeWorkflowId);
}
}, [resumeWorkflowId, workflowState.currentWorkflowId, workflowActions]);
const handleMouseMove = (e: MouseEvent) => {
if (!isDragging) return;
// No resizing functionality needed
const container = document.querySelector('.chat-grid') as HTMLElement;
if (!container) return;
console.log('🎯 DashboardChatArea render:', {
currentWorkflowId: workflowState.currentWorkflowId,
resumeWorkflowId,
workflowStatus: workflowState.workflow?.status,
messagesCount: workflowState.messages?.length || 0,
isLoading: workflowState.isLoading,
isPolling: workflowState.isPolling
});
const rect = container.getBoundingClientRect();
return (
<div className={styles.chat_grid}>
{/* Top Left: Message List */}
<div className={`${styles.quadrant} ${styles.messages_quadrant}`}>
<MessageList
workflowState={workflowState}
onFilePreview={setSelectedFile}
/>
</div>
if (isDragging === 'horizontal') {
const newSplit = ((e.clientY - rect.top) / rect.height) * 100;
setHorizontalSplit(Math.max(20, Math.min(80, newSplit)));
} else if (isDragging === 'vertical') {
const newSplit = ((e.clientX - rect.left) / rect.width) * 100;
setVerticalSplit(Math.max(20, Math.min(80, newSplit)));
}
};
{/* Top Right: File Preview */}
<div className={`${styles.quadrant} ${styles.file_preview_quadrant}`}>
<FilePreview selectedFile={selectedFile} />
</div>
const handleMouseUp = () => {
setIsDragging(null);
};
{/* Bottom Left: Input Area */}
<div className={`${styles.quadrant} ${styles.input_quadrant}`}>
<InputArea
selectedPrompt={selectedPrompt}
onPromptUsed={onPromptUsed}
workflowState={workflowState}
workflowActions={workflowActions}
onAttachedFilesChange={setAttachedFiles}
attachedFiles={attachedFiles}
/>
</div>
// Event listeners
React.useEffect(() => {
if (isDragging) {
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
document.body.style.cursor = isDragging === 'horizontal' ? 'ns-resize' : 'ew-resize';
document.body.style.userSelect = 'none';
return () => {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
document.body.style.cursor = '';
document.body.style.userSelect = '';
};
}
}, [isDragging]);
return (
<div
className="chat-grid"
style={{
gridTemplateRows: `${horizontalSplit}% 1px ${100 - horizontalSplit}%`,
gridTemplateColumns: `${verticalSplit}% 1px ${100 - verticalSplit}%`
}}
>
{/* Top Left: Message List */}
<div className="quadrant messages-quadrant">
<MessageList
selectedPrompt={selectedPrompt}
onPromptUsed={onPromptUsed}
resumeWorkflowId={currentWorkflowId}
onFilePreview={setSelectedFile}
/>
</div>
{/* Vertical Divider */}
<div
className="divider vertical-divider"
onMouseDown={handleMouseDown('vertical')}
/>
{/* Top Right: File Preview */}
<div className="quadrant file-preview-quadrant">
<FilePreview selectedFile={selectedFile} />
</div>
{/* Horizontal Divider */}
<div
className="divider horizontal-divider"
onMouseDown={handleMouseDown('horizontal')}
/>
{/* Bottom Left: Input Area */}
<div className="quadrant input-quadrant">
<InputArea
selectedPrompt={selectedPrompt}
onPromptUsed={onPromptUsed}
onWorkflowIdChange={handleWorkflowIdChange}
onAttachedFilesChange={setAttachedFiles}
attachedFiles={attachedFiles}
/>
</div>
{/* Bottom Right: Connected Files */}
<div className="quadrant connected-files-quadrant">
<ConnectedFiles
onFileSelect={setSelectedFile}
selectedFile={selectedFile}
attachedFiles={attachedFiles}
onRemoveFile={(fileId) => {
// If the removed file is currently selected, clear the selection
if (selectedFile?.id === fileId) {
setSelectedFile(null);
}
// Remove the file from attached files
setAttachedFiles(files => files.filter(f => f.id !== fileId));
}}
/>
</div>
</div>
);
{/* Bottom Right: Connected Files */}
<div className={`${styles.quadrant} ${styles.connected_files_quadrant}`}>
<ConnectedFiles
onFileSelect={setSelectedFile}
selectedFile={selectedFile}
attachedFiles={attachedFiles}
onRemoveFile={(fileId) => {
// If the removed file is currently selected, clear the selection
if (selectedFile?.id === fileId) {
setSelectedFile(null);
}
// Remove the file from attached files
setAttachedFiles(files => files.filter(f => f.id !== fileId));
}}
/>
</div>
</div>
);
};
export default DashboardChatArea;

View file

@ -75,7 +75,6 @@ const ConnectedFiles: React.FC<ConnectedFilesProps> = ({
return (
<div style={{ padding: '16px', height: '100%', overflow: 'auto' }}>
<h3>Connected Files</h3>
{/* Show attached files count */}
{attachedFiles.length > 0 && (

View file

@ -91,7 +91,7 @@ const FilePreview: React.FC<FilePreviewProps> = ({ selectedFile }) => {
return (
<div style={{ padding: '16px', height: '100%', overflow: 'auto' }}>
<h3>File Preview</h3>
{!selectedFile && (
<p style={{ color: 'var(--color-gray)', textAlign: 'center' }}>

View file

@ -1,187 +1,243 @@
import React, { useState, useEffect } from 'react';
import { useWorkflowOperations } from '../../../hooks/useWorkflows';
import React, { useState, useEffect, useRef } from 'react';
import { Prompt } from '../../../hooks/usePrompts';
import FileAttachmentPopup from './FileAttachmentPopup';
import { WorkflowManagerState, WorkflowManagerActions } from './useWorkflowManager';
import { useLanguage } from '../../../contexts/LanguageContext';
import styles from './DashboardChatAreaStyles/DashboardChatAreaInput.module.css';
import sharedStyles from './DashboardChatAreaStyles/DashboardChat.module.css';
interface InputAreaProps {
selectedPrompt?: Prompt | null;
onPromptUsed?: () => void;
onWorkflowIdChange?: (workflowId: string | null) => void;
onAttachedFilesChange?: (files: AttachedFile[]) => void;
attachedFiles?: AttachedFile[];
selectedPrompt?: Prompt | null;
onPromptUsed?: () => void;
workflowState: WorkflowManagerState;
workflowActions: WorkflowManagerActions;
onAttachedFilesChange?: (files: AttachedFile[]) => void;
attachedFiles?: AttachedFile[];
}
interface AttachedFile {
id: number;
name: string;
size: number;
type: string;
fileData?: File;
objectUrl?: string;
id: number;
name: string;
size: number;
type: string;
fileData?: File;
objectUrl?: string;
}
const InputArea: React.FC<InputAreaProps> = ({
selectedPrompt,
onPromptUsed,
onWorkflowIdChange,
onAttachedFilesChange,
attachedFiles: externalAttachedFiles = []
selectedPrompt,
onPromptUsed,
workflowState,
workflowActions,
onAttachedFilesChange,
attachedFiles: externalAttachedFiles = []
}) => {
const [inputValue, setInputValue] = useState('');
const [showFilePopup, setShowFilePopup] = useState(false);
const { t } = useLanguage();
const [inputValue, setInputValue] = useState('');
const [showFilePopup, setShowFilePopup] = useState(false);
const [isSending, setIsSending] = useState(false);
const [sendError, setSendError] = useState<string | null>(null);
const [isFocused, setIsFocused] = useState(false);
const textareaRef = useRef<HTMLTextAreaElement>(null);
// Always use external attached files from parent component
const currentAttachedFiles = externalAttachedFiles;
const { startWorkflow, startingWorkflow, startError } = useWorkflowOperations();
// Always use external attached files from parent component
const currentAttachedFiles = externalAttachedFiles;
// Auto-fill input when prompt is selected
useEffect(() => {
if (selectedPrompt) {
setInputValue(selectedPrompt.content);
}
}, [selectedPrompt]);
// Auto-resize textarea function
const adjustTextareaHeight = () => {
const textarea = textareaRef.current;
if (!textarea) return;
const handleSend = async () => {
if (!inputValue.trim() || startingWorkflow) return;
// Reset height to auto to get the actual scroll height
textarea.style.height = 'auto';
// Calculate the height based on content
const scrollHeight = textarea.scrollHeight;
const lineHeight = 1.5 * 14; // 1.5em * 14px font size
const padding = 32; // 16px top + 16px bottom padding
const minHeight = lineHeight * 4 + padding; // 4 rows
const maxHeight = lineHeight * 8 + padding; // 8 rows
// Set height within constraints
const newHeight = Math.min(Math.max(scrollHeight, minHeight), maxHeight);
textarea.style.height = `${newHeight}px`;
};
try {
const result = await startWorkflow({
prompt: inputValue,
listFileId: currentAttachedFiles.map(f => f.id)
});
// Auto-fill input when prompt is selected
useEffect(() => {
if (selectedPrompt) {
setInputValue(selectedPrompt.content);
}
}, [selectedPrompt]);
if (result.success) {
setInputValue('');
if (onAttachedFilesChange) {
onAttachedFilesChange([]);
}
if (onPromptUsed) onPromptUsed();
if (onWorkflowIdChange && result.data?.id) {
onWorkflowIdChange(result.data.id);
}
}
} catch (error) {
console.error('Failed to start workflow:', error);
}
};
// Adjust height when input value changes
useEffect(() => {
adjustTextareaHeight();
}, [inputValue]);
const handleKeyPress = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSend();
}
};
// Initial resize on mount
useEffect(() => {
adjustTextareaHeight();
}, []);
const handleFilesAttached = (files: AttachedFile[]) => {
setShowFilePopup(false);
const handleSend = async () => {
if (!inputValue.trim() || isSending) return;
setIsSending(true);
setSendError(null);
try {
const fileIds = currentAttachedFiles.map(f => f.id);
let success = false;
if (workflowState.currentWorkflowId) {
// Continue existing workflow
console.log(`➡️ Continuing workflow ${workflowState.currentWorkflowId}`);
success = await workflowActions.continueWorkflow(inputValue, fileIds);
} else {
// Start new workflow
console.log('🚀 Starting new workflow');
const newWorkflowId = await workflowActions.startNewWorkflow(inputValue, fileIds);
success = !!newWorkflowId;
}
if (success) {
setInputValue('');
if (onAttachedFilesChange) {
onAttachedFilesChange(files);
onAttachedFilesChange([]);
}
};
if (onPromptUsed) onPromptUsed();
} else {
setSendError('Failed to send message. Please try again.');
}
} catch (error: any) {
console.error('Failed to send message:', error);
setSendError(error.message || 'Failed to send message. Please try again.');
} finally {
setIsSending(false);
}
};
const formatFileSize = (bytes: number): string => {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return Math.round(bytes / 1024) + ' KB';
return Math.round(bytes / (1024 * 1024)) + ' MB';
};
const handleKeyPress = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSend();
}
};
return (
<div style={{ padding: '16px', height: '100%', display: 'flex', flexDirection: 'column' }}>
<h3>Input</h3>
{startError && (
<div style={{
padding: '8px',
backgroundColor: '#ffe6e6',
color: '#d00',
borderRadius: '4px',
marginBottom: '12px'
}}>
Error: {startError}
</div>
)}
const handleFilesAttached = (files: AttachedFile[]) => {
setShowFilePopup(false);
if (onAttachedFilesChange) {
onAttachedFilesChange(files);
}
};
{/* Show attached files count */}
{currentAttachedFiles.length > 0 && (
<div style={{
marginBottom: '8px',
padding: '6px 10px',
backgroundColor: '#e3f2fd',
borderRadius: '4px',
fontSize: '12px',
color: '#1976d2',
textAlign: 'center'
}}>
📎 {currentAttachedFiles.length} file{currentAttachedFiles.length !== 1 ? 's' : ''} attached
</div>
)}
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px', flex: 1 }}>
<textarea
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyPress={handleKeyPress}
placeholder="Enter your message or prompt..."
disabled={startingWorkflow}
style={{
flex: 1,
padding: '12px',
border: '1px solid var(--color-gray-disabled)',
borderRadius: '8px',
resize: 'none',
fontSize: '14px',
fontFamily: 'inherit'
}}
/>
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
<button
onClick={() => setShowFilePopup(true)}
style={{
padding: '8px 12px',
backgroundColor: 'var(--color-surface)',
border: '1px solid var(--color-gray-disabled)',
borderRadius: '6px',
cursor: 'pointer',
fontSize: '14px'
}}
>
📎 Attach Files
</button>
<button
onClick={handleSend}
disabled={!inputValue.trim() || startingWorkflow}
style={{
padding: '8px 16px',
backgroundColor: startingWorkflow ? 'var(--color-gray-disabled)' : 'var(--color-secondary)',
color: 'white',
border: 'none',
borderRadius: '6px',
cursor: startingWorkflow ? 'not-allowed' : 'pointer'
}}
>
{startingWorkflow ? 'Starting...' : 'Send'}
</button>
{selectedPrompt && (
<span style={{ fontSize: '12px', color: 'var(--color-gray)' }}>
Using prompt: {selectedPrompt.name}
</span>
)}
</div>
</div>
{/* File Attachment Popup */}
{showFilePopup && (
<FileAttachmentPopup
onClose={() => setShowFilePopup(false)}
onFilesSelected={handleFilesAttached}
currentAttachedFiles={currentAttachedFiles}
/>
)}
const isWorkflowActive = workflowState.workflow &&
['running', 'processing', 'started'].includes(workflowState.workflow.status);
// Determine if label should be in focused/moved state
const shouldLabelBeFocused = isFocused || inputValue.trim().length > 0;
// Get placeholder text
const placeholderText = workflowState.currentWorkflowId
? t('chat.input.continue_workflow')
: t('chat.input.enter_message');
return (
<div className={styles.input_area_container}>
{/* Error messages */}
{(sendError || workflowState.error) && (
<div className={styles.error_message}>
{t('chat.input.error_prefix')} {sendError || workflowState.error}
</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')}
</div>
)}
<div className={styles.input_form_container}>
<div className={styles.floating_label_textarea}>
<label
className={shouldLabelBeFocused ? styles.textarea_label_focused : styles.textarea_label}
>
{placeholderText}
</label>
<textarea
ref={textareaRef}
value={inputValue}
onChange={(e) => {
setInputValue(e.target.value);
// Trigger resize on next frame to ensure DOM is updated
setTimeout(adjustTextareaHeight, 0);
}}
onKeyPress={handleKeyPress}
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
placeholder=""
disabled={isSending || isWorkflowActive}
className={styles.message_textarea}
rows={4}
/>
</div>
<div className={styles.input_actions_row}>
<button
onClick={() => setShowFilePopup(true)}
disabled={isSending || isWorkflowActive}
className={sharedStyles.button_secondary}
>
{t('chat.input.attach_files')}
</button>
<button
onClick={handleSend}
disabled={!inputValue.trim() || isSending || isWorkflowActive}
className={`${sharedStyles.button_primary} ${
(!inputValue.trim() || isSending || isWorkflowActive)
? styles.disabled
: styles.enabled
}`}
>
{isSending ? t('chat.input.sending') :
isWorkflowActive ? t('chat.input.processing') :
workflowState.currentWorkflowId ? t('chat.input.continue') : t('chat.input.send')}
</button>
{workflowState.currentWorkflowId && !isWorkflowActive && (
<button
onClick={() => workflowActions.clearWorkflow()}
className={styles.new_chat_button}
>
{t('chat.input.new_chat')}
</button>
)}
{selectedPrompt && (
<span className={styles.prompt_indicator}>
{t('chat.input.using_prompt')} {selectedPrompt.name}
</span>
)}
</div>
</div>
{/* File Attachment Popup */}
{showFilePopup && (
<FileAttachmentPopup
onClose={() => setShowFilePopup(false)}
onFilesSelected={handleFilesAttached}
currentAttachedFiles={currentAttachedFiles}
/>
)}
</div>
);
};
export default InputArea;

View file

@ -69,6 +69,9 @@ const MessageItem: React.FC<MessageItemProps> = ({ message, index, onFilePreview
console.log(`🎭 MessageItem rendering:`, {
messageId: message.id,
messageRole: message.role,
content: message.content?.substring(0, 50) + (message.content?.length > 50 ? '...' : ''),
contentLength: message.content?.length || 0,
hasContent: !!message.content,
hasDocuments: !!(message.documents),
documentsArray: message.documents,
documentsLength: message.documents?.length || 0,
@ -171,7 +174,14 @@ const MessageItem: React.FC<MessageItemProps> = ({ message, index, onFilePreview
lineHeight: '1.5',
whiteSpace: 'pre-wrap'
}}>
{message.content}
{message.content || (
<span style={{
color: 'var(--color-gray)',
fontStyle: 'italic'
}}>
[No message content]
</span>
)}
</div>
{hasDocuments && (

View file

@ -1,255 +1,348 @@
import React, { useEffect, useRef, useState, useCallback } from 'react';
import { useWorkflowStatus } from '../../../hooks/useWorkflows';
import { Prompt } from '../../../hooks/usePrompts';
import React from 'react';
import { useApiRequest } from '../../../hooks/useApi';
import MessageItem from './DashboardChatAreaMessageItem';
import { Message, Document, WorkflowMessage } from './dashboardChatAreaTypes';
import { WorkflowMessage, Document } from './dashboardChatAreaTypes';
import { WorkflowManagerState } from './useWorkflowManager';
interface MessageListProps {
selectedPrompt?: Prompt | null;
onPromptUsed?: () => void;
resumeWorkflowId?: string | null;
onFilePreview?: (file: any) => void;
workflowState: WorkflowManagerState;
onFilePreview?: (file: any) => void;
}
// Custom hook to fetch and transform messages like the old code
const useTransformedMessages = (workflowId: string | null) => {
const [messages, setMessages] = useState<Message[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const { request } = useApiRequest();
// Helper function to transform WorkflowMessage to display Message
const transformWorkflowMessage = async (workflowMessage: WorkflowMessage, request: any): Promise<any> => {
let documents: Document[] = [];
const fetchMessages = useCallback(async () => {
if (!workflowId) {
setMessages([]);
return;
}
// Fetch file metadata if fileIds exist
if (workflowMessage.fileIds && workflowMessage.fileIds.length > 0) {
console.log(`📎 Processing ${workflowMessage.fileIds.length} files for message ${workflowMessage.id}`);
const documentPromises = workflowMessage.fileIds.map(async (fileId, fileIndex) => {
try {
console.log(`📁 Fetching metadata for file ${fileIndex + 1}/${workflowMessage.fileIds!.length}: ${fileId}`);
const response = await request({
url: `/api/workflows/files/${fileId}/preview`,
method: 'get'
});
const document: Document = {
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
};
setLoading(true);
setError(null);
console.log(`✅ File ${fileId} metadata processed:`, document.name);
return document;
} catch (error) {
console.error(`❌ Failed to fetch metadata for file ${fileId}:`, error);
// Return a fallback object for failed requests
return {
id: fileId.toString(),
fileId: fileId,
name: `File_${fileId}`,
ext: 'unknown',
type: 'application/octet-stream',
size: 0
};
}
});
try {
console.log(`🔍 Fetching messages for workflow: ${workflowId}`);
// Fetch workflow messages
const workflowMessages: WorkflowMessage[] = await request({
url: `/api/workflows/${workflowId}/messages`,
method: 'get'
});
documents = await Promise.all(documentPromises);
}
console.log(`📨 Received ${workflowMessages.length} messages from API:`, workflowMessages);
// Try different possible field names for content
const possibleContent = workflowMessage.content ||
(workflowMessage as any).message ||
(workflowMessage as any).text ||
(workflowMessage as any).body ||
'';
// Debug each message structure
workflowMessages.forEach((msg, index) => {
console.log(`📄 Message ${index + 1}:`, {
id: msg.id,
role: msg.role,
content: msg.content?.substring(0, 50) + '...',
fileIds: msg.fileIds,
hasFileIds: !!(msg.fileIds && msg.fileIds.length > 0),
fileIdsLength: msg.fileIds?.length || 0
});
});
const transformedMessage = {
id: workflowMessage.id,
role: workflowMessage.role,
agentName: workflowMessage.role === 'user' ? 'You' : 'Assistant',
content: possibleContent,
timestamp: workflowMessage.timestamp,
documents: documents
};
// Transform each message
const transformedMessages = await Promise.all(
workflowMessages.map(async (workflowMessage: WorkflowMessage, msgIndex) => {
console.log(`🔄 Transforming message ${msgIndex + 1} (${workflowMessage.id})`);
let documents: Document[] = [];
console.log(`🔄 Transformation result for ${workflowMessage.id}:`, {
originalMessage: workflowMessage,
originalContent: workflowMessage.content,
originalContentType: typeof workflowMessage.content,
possibleContent: possibleContent,
possibleContentType: typeof possibleContent,
transformedContent: transformedMessage.content,
transformedContentType: typeof transformedMessage.content,
allOriginalKeys: Object.keys(workflowMessage),
// Check for alternative field names
alternativeFields: {
message: (workflowMessage as any).message,
text: (workflowMessage as any).text,
body: (workflowMessage as any).body,
data: (workflowMessage as any).data
}
});
// Fetch file metadata if fileIds exist
if (workflowMessage.fileIds && workflowMessage.fileIds.length > 0) {
console.log(`📎 Message ${workflowMessage.id} has ${workflowMessage.fileIds.length} fileIds:`, workflowMessage.fileIds);
const documentPromises = workflowMessage.fileIds.map(async (fileId, fileIndex) => {
try {
console.log(`📁 Fetching metadata for file ${fileIndex + 1}/${workflowMessage.fileIds!.length}: ${fileId}`);
const response = await request({
url: `/api/workflows/files/${fileId}/preview`,
method: 'get'
});
console.log(`✅ File ${fileId} metadata received:`, response);
const document: Document = {
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
};
console.log(`🗂️ Created document object:`, document);
return document;
} catch (error) {
console.error(`❌ Failed to fetch metadata for file ${fileId}:`, error);
// Return a fallback object for failed requests
const fallbackDoc: Document = {
id: fileId.toString(),
fileId: fileId,
name: `File_${fileId}`,
ext: 'unknown',
type: 'application/octet-stream',
size: 0
};
console.log(`🔧 Created fallback document:`, fallbackDoc);
return fallbackDoc;
}
});
documents = await Promise.all(documentPromises);
console.log(`📋 All files processed for message ${workflowMessage.id}. Total documents: ${documents.length}`);
} else {
console.log(`📭 Message ${workflowMessage.id} has no fileIds`);
}
// Transform to old Message format
const message: Message = {
id: workflowMessage.id,
role: workflowMessage.role,
agentName: workflowMessage.role === 'user' ? 'You' : 'Assistant',
content: workflowMessage.content,
timestamp: workflowMessage.timestamp,
documents: documents
};
console.log(`✨ Final transformed message:`, {
id: message.id,
role: message.role,
documentsCount: message.documents?.length || 0,
hasDocuments: !!(message.documents && message.documents.length > 0)
});
return message;
})
);
console.log(`🎉 Successfully transformed all ${transformedMessages.length} messages`);
console.log(`📊 Summary:`, transformedMessages.map(msg => ({
id: msg.id,
role: msg.role,
documentsCount: msg.documents?.length || 0
})));
setMessages(transformedMessages);
} catch (err: any) {
console.error('💥 Error fetching messages:', err);
setError(err.message || 'Failed to fetch messages');
} finally {
setLoading(false);
}
}, [workflowId, request]);
useEffect(() => {
fetchMessages();
}, [fetchMessages]);
return { messages, loading, error, refetch: fetchMessages };
return transformedMessage;
};
const MessageList: React.FC<MessageListProps> = ({
selectedPrompt,
onPromptUsed,
resumeWorkflowId,
onFilePreview
workflowState,
onFilePreview
}) => {
const { messages, loading, error, refetch } = useTransformedMessages(resumeWorkflowId || null);
const { status } = useWorkflowStatus(resumeWorkflowId || null);
const intervalRef = useRef<number | null>(null);
const [isInitialLoad, setIsInitialLoad] = React.useState(true);
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);
// Auto-refresh messages every 3 seconds when workflow is active
useEffect(() => {
if (resumeWorkflowId && status?.status &&
(status.status === 'running' || status.status === 'processing' || status.status === 'started')) {
intervalRef.current = window.setInterval(() => {
console.log('🔄 Auto-refreshing messages due to active workflow');
refetch();
}, 3000);
} else {
// Stop polling when completed, failed, or no workflow
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
}
// Transform messages when workflow messages change
React.useEffect(() => {
const transformMessages = async () => {
if (!workflowState.messages || workflowState.messages.length === 0) {
setTransformedMessages([]);
return;
}
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
};
}, [resumeWorkflowId, status?.status, refetch]);
setIsTransforming(true);
console.log(`🔄 Transforming ${workflowState.messages.length} workflow messages...`);
// Initial load when workflow ID changes
useEffect(() => {
if (resumeWorkflowId) {
console.log(`🚀 Starting initial load for workflow: ${resumeWorkflowId}`);
setIsInitialLoad(true);
refetch().finally(() => setIsInitialLoad(false));
} else {
setIsInitialLoad(false);
}
}, [resumeWorkflowId, refetch]);
return (
<div style={{ padding: '16px', height: '100%', overflow: 'auto' }}>
<h3>Messages</h3>
try {
const transformed = await Promise.all(
workflowState.messages.map(async (msg: WorkflowMessage, index: number) => {
console.log(`🔄 Transforming message ${index + 1}/${workflowState.messages.length}: ${msg.id}`);
console.log(`📝 RAW API Message (${msg.role}):`, {
id: msg.id,
rawMessage: msg,
contentType: typeof msg.content,
contentValue: msg.content,
contentLength: msg.content?.length || 0,
hasContent: !!msg.content,
contentPreview: msg.content?.substring(0, 100) + (msg.content?.length > 100 ? '...' : ''),
fileCount: msg.fileIds?.length || 0,
allKeys: Object.keys(msg)
});
{error && <p style={{ color: 'red' }}>Error: {error}</p>}
{status && (
<div style={{
padding: '8px',
backgroundColor: 'var(--color-surface)',
borderRadius: '4px',
marginBottom: '16px'
}}>
<strong>Status:</strong> {status.status}
{status.currentRound && ` (Round ${status.currentRound})`}
{/* Show a small indicator when polling for updates */}
{intervalRef.current && (
<span style={{
marginLeft: '8px',
fontSize: '12px',
color: 'var(--color-secondary)',
opacity: 0.7
}}>
🔄 Live updates
</span>
)}
</div>
)}
return await transformWorkflowMessage(msg, request);
})
);
<div style={{ display: 'flex', flexDirection: 'column' }}>
{messages.map((message, index) => {
console.log(`🎨 Rendering message ${message.id} with ${message.documents?.length || 0} documents`);
return (
<MessageItem
key={message.id}
message={message}
index={index}
onFilePreview={onFilePreview}
/>
);
})}
</div>
console.log(`✅ Successfully transformed ${transformed.length} messages`);
setTransformedMessages(transformed);
} catch (error) {
console.error('❌ Error transforming messages:', error);
setTransformedMessages([]);
} finally {
setIsTransforming(false);
}
};
{loading && (
<div style={{ textAlign: 'center', padding: '16px' }}>
<p style={{ color: 'var(--color-gray)' }}>Loading messages...</p>
</div>
)}
transformMessages();
}, [workflowState.messages, request]);
{messages.length === 0 && !isInitialLoad && !loading && (
<p style={{ color: 'var(--color-gray)', textAlign: 'center' }}>
No messages yet. Start a workflow to see messages here.
</p>
)}
// Check if user is scrolled near the bottom
const checkScrollPosition = React.useCallback(() => {
const container = scrollContainerRef.current;
if (!container) return;
const { scrollTop, scrollHeight, clientHeight } = container;
const distanceFromBottom = scrollHeight - scrollTop - clientHeight;
// Consider "near bottom" if within 100px of the bottom
const isNearBottom = distanceFromBottom < 100;
setIsUserScrolledUp(!isNearBottom);
console.log('📏 Scroll position:', {
scrollTop,
scrollHeight,
clientHeight,
distanceFromBottom,
isNearBottom,
isUserScrolledUp: !isNearBottom
});
}, []);
// Scroll to bottom function
const scrollToBottom = React.useCallback(() => {
const container = scrollContainerRef.current;
if (container) {
console.log('⬇️ Auto-scrolling to bottom');
container.scrollTop = container.scrollHeight;
}
}, []);
// Auto-scroll when new messages arrive (only if user is near bottom)
React.useEffect(() => {
const currentMessageCount = transformedMessages.length;
const hadMessages = lastMessageCountRef.current > 0;
const hasNewMessages = currentMessageCount > lastMessageCountRef.current;
if (hasNewMessages && hadMessages && !isUserScrolledUp) {
console.log('🆕 New messages detected, auto-scrolling to bottom');
// Small delay to ensure DOM is updated
setTimeout(scrollToBottom, 100);
}
lastMessageCountRef.current = currentMessageCount;
}, [transformedMessages.length, isUserScrolledUp, scrollToBottom]);
// Scroll to bottom on initial load
React.useEffect(() => {
if (transformedMessages.length > 0 && lastMessageCountRef.current === 0) {
console.log('📜 Initial load, scrolling to bottom');
setTimeout(scrollToBottom, 100);
}
}, [transformedMessages.length, scrollToBottom]);
const { currentWorkflowId, workflow, isLoading, error, isPolling } = workflowState;
return (
<>
{/* Add CSS animations */}
<style>
{`
@keyframes pulse {
0%, 100% { opacity: 0.7; }
50% { opacity: 1; }
}
.scroll-to-bottom-btn:hover {
transform: scale(1.1);
box-shadow: 0 4px 12px rgba(0,0,0,0.3) !important;
}
.scroll-to-bottom-btn:active {
transform: scale(0.95);
}
`}
</style>
<div
ref={scrollContainerRef}
style={{
padding: '16px',
height: '100%',
overflow: 'auto'
}}
onScroll={checkScrollPosition}
>
{error && (
<div style={{
padding: '8px',
backgroundColor: 'var(--color-error)',
color: 'white',
borderRadius: '4px',
marginBottom: '16px'
}}>
Error: {error}
</div>
);
)}
{workflow && (
<div style={{
padding: '8px',
backgroundColor: 'var(--color-surface)',
borderRadius: '4px',
marginBottom: '16px'
}}>
<strong>Workflow:</strong> {workflow.id}
<br />
<strong>Status:</strong> {workflow.status}
{workflow.currentRound && ` (Round ${workflow.currentRound})`}
{/* Show polling indicator */}
{isPolling && (
<span style={{
marginLeft: '8px',
fontSize: '12px',
color: 'var(--color-secondary)',
opacity: 0.7,
animation: 'pulse 2s infinite'
}}>
🔄 Live updates (2s)
</span>
)}
</div>
)}
<div style={{ display: 'flex', flexDirection: 'column' }}>
{transformedMessages.map((message, index) => {
console.log(`🎨 Rendering transformed message ${message.id}:`, {
role: message.role,
contentLength: message.content?.length || 0,
hasContent: !!message.content,
documentsCount: message.documents?.length || 0
});
return (
<MessageItem
key={message.id}
message={message}
index={index}
onFilePreview={onFilePreview}
/>
);
})}
</div>
{(isLoading || isTransforming) && (
<div style={{ textAlign: 'center', padding: '16px' }}>
<p style={{ color: 'var(--color-gray)' }}>
{isTransforming ? 'Processing messages...' : 'Loading messages...'}
</p>
</div>
)}
{transformedMessages.length === 0 && !isLoading && !isTransforming && !currentWorkflowId && (
<p style={{ color: 'var(--color-gray)', textAlign: 'center' }}>
No workflow selected. Start a conversation to create a new workflow.
</p>
)}
{transformedMessages.length === 0 && !isLoading && !isTransforming && currentWorkflowId && (
<p style={{ color: 'var(--color-gray)', textAlign: 'center' }}>
No messages in this workflow yet.
</p>
)}
</div>
{/* Scroll to bottom button - positioned relative to parent container, not scrollable area */}
<button
className="scroll-to-bottom-btn"
onClick={scrollToBottom}
style={{
position: 'absolute',
bottom: '20px',
right: '20px',
backgroundColor: 'var(--color-secondary)',
color: 'white',
border: 'none',
borderRadius: '50%',
width: '48px',
height: '48px',
fontSize: '20px',
cursor: 'pointer',
boxShadow: '0 2px 8px rgba(0,0,0,0.2)',
zIndex: 1000,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
opacity: (isUserScrolledUp && transformedMessages.length > 0) ? 1 : 0,
visibility: (isUserScrolledUp && transformedMessages.length > 0) ? 'visible' : 'hidden',
transition: 'opacity 0.3s ease-in-out, visibility 0.3s ease-in-out, transform 0.2s ease-in-out',
pointerEvents: (isUserScrolledUp && transformedMessages.length > 0) ? 'auto' : 'none'
}}
title="Scroll to bottom"
>
</button>
</>
);
};
export default MessageList;

View file

@ -1,16 +1,271 @@
.dashboard_chat {
display: flex;
padding: 20px;
flex-direction: column; /* Fixed: was 'space-between' which is invalid */
flex-direction: column;
align-self: stretch;
background: var(--color-bg);
position: relative;
box-shadow: 0px 2px 6px 0px rgba(194, 194, 194, 0.10);
height: 100%;
flex: 1;
min-height: 0;
height: 100%; /* Fill parent height */
flex: 1; /* Take all available space from parent */
overflow: hidden;
font-family: var(--font-family);
}
/* Grid Layout */
.chat_grid {
display: grid;
width: 100%;
height: 100%;
grid-template-rows: 1fr auto;
grid-template-columns: 2fr 1fr;
gap: 0px;
overflow: hidden;
box-sizing: border-box;
position: relative;
}
.quadrant {
overflow: hidden;
background-color: var(--color-bg);
display: flex;
flex-direction: column;
min-height: 0;
min-width: 0;
box-sizing: border-box;
}
/* Quadrant specific styles */
.messages_quadrant {
grid-row: 1;
grid-column: 1;
}
.file_preview_quadrant {
grid-row: 1;
grid-column: 2;
}
.input_quadrant {
grid-row: 2;
grid-column: 1;
}
.connected_files_quadrant {
grid-row: 2;
grid-column: 2;
}
/* Chat Messages */
.chat_messages {
flex: 1;
padding: 15px;
border-radius: 15px;
margin-bottom: 15px;
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
scroll-behavior: smooth;
}
.chat_messages::-webkit-scrollbar {
width: 6px;
}
.chat_messages::-webkit-scrollbar-track {
background: transparent;
}
.chat_messages::-webkit-scrollbar-thumb {
background: var(--color-gray-disabled);
border-radius: 3px;
}
.chat_messages::-webkit-scrollbar-thumb:hover {
background: var(--color-gray);
}
.messages_container {
min-height: 100%;
display: flex;
flex-direction: column;
justify-content: flex-start;
}
.messages_spacer {
flex: 1;
min-height: 20px;
}
/* Chat Input */
.chat_input {
width: 100%;
height: 40px;
padding: 6px 10px;
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.6;
min-width: 120px;
transition: all 0.2s ease;
box-sizing: border-box;
}
.chat_input.drag_over {
background-color: var(--color-secondary-disabled);
border: 2px dashed var(--color-secondary);
border-radius: 12px;
padding: 8px;
}
.input_row {
display: flex;
gap: 10px;
align-items: flex-end;
width: 100%;
}
.message_input {
flex: 1;
padding: 12px 16px;
border: 1px solid var(--color-gray-disabled);
border-radius: 12px;
outline: none;
font-size: 14px;
font-family: var(--font-family);
background-color: var(--color-bg);
color: var(--color-text);
}
.message_input:focus {
border-color: var(--color-secondary);
}
.message_input:disabled {
background-color: var(--color-surface);
cursor: not-allowed;
opacity: 0.6;
}
.button_secondary {
border: 2px dashed var(--color-secondary);
border-radius: 30px;
padding: 10px 20px;
background: var(--color-bg);
cursor: pointer;
transition: all 0.2s ease;
min-width: 100px;
text-align: center;
font-family: var(--font-family);
color: var(--color-secondary);
}
.button_secondary:hover {
background-color: var(--color-secondary);
color: var(--color-bg);
}
.button_secondary:disabled {
background-color: var(--color-bg);
cursor: not-allowed;
opacity: 0.6;
color: var(--color-secondary);
}
.button_primary {
border-radius: 30px;
border: 1px solid var(--color-secondary);
background: var(--color-secondary);
color: var(--color-bg);
border: none;
outline: none;
padding: 10px 20px;
display: flex;
gap: 10px;
align-items: center;
flex-shrink: 0;
transition: background-color 0.2s ease;
font-family: var(--font-family);
cursor: pointer;
min-width: 100px;
align-items: center;
justify-content: center;
}
.button_primary:hover {
background-color: var(--color-secondary-hover);
}
.button_primary:disabled {
background-color: var(--color-gray-disabled);
border: 1px solid var(--color-gray-disabled);
cursor: not-allowed;
opacity: 0.6;
border: 1px solid var(--color-gray-disabled);
}
/* Attached Files */
.attached_files {
display: flex;
flex-wrap: wrap;
gap: 8px;
width: 100%;
margin-bottom: 5px;
}
.attached_file {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 8px;
background-color: var(--color-secondary-disabled);
border: 1px solid var(--color-secondary);
border-radius: 8px;
font-size: 12px;
color: var(--color-secondary);
font-family: var(--font-family);
}
.attached_file_icon {
font-size: 16px;
}
.attached_file_name {
max-width: 150px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.attached_file_remove {
background: none;
border: none;
color: var(--color-gray);
cursor: pointer;
padding: 0;
margin-left: 4px;
display: flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
border-radius: 50%;
transition: background-color 0.2s ease, color 0.2s ease;
}
.attached_file_remove:hover {
background-color: var(--color-gray-disabled);
color: var(--color-text);
}
/* Input Area styles moved to DashboardChatAreaInput.module.css */

View file

@ -1,843 +0,0 @@
.chat_area {
display: flex;
flex-direction: column;
align-items: stretch;
flex: 1;
height: 100%;
overflow: hidden;
font-family: var(--font-family);
}
/* Grid Container for Four Quadrants */
.grid_container {
display: grid;
width: 100%;
height: 100%;
overflow: hidden;
gap: 0;
}
/* Quadrant Base Styles */
.quadrant {
overflow: hidden;
background-color: var(--color-bg);
display: flex;
flex-direction: column;
min-width: 0;
min-height: 0;
}
.quadrant_header {
padding: 10px 15px;
border-bottom: 1px solid var(--color-gray-disabled);
background-color: var(--color-surface);
flex-shrink: 0;
}
.quadrant_header h3 {
margin: 0;
font-size: 14px;
font-weight: 500;
color: var(--color-text);
}
.quadrant_content {
flex: 1;
padding: 15px;
overflow-y: auto;
overflow-x: hidden;
}
/* Specific Quadrant Styles */
.messages_quadrant {
border-right: 1px solid var(--color-primary);
border-bottom: 1px solid var(--color-primary);
}
.file_preview_quadrant {
border-bottom: 1px solid var(--color-primary);
}
.input_quadrant {
border-right: 1px solid var(--color-primary);
}
.connected_files_quadrant {
/* No additional borders needed */
}
/* Resizable Dividers */
.vertical_divider {
background-color: var(--color-primary);
cursor: ew-resize;
grid-row: 1 / -1;
width: 1px;
transition: background-color 0.2s ease;
}
.vertical_divider:hover {
background-color: var(--color-secondary);
width: 3px;
}
.horizontal_divider {
background-color: var(--color-primary);
cursor: ns-resize;
height: 1px;
transition: background-color 0.2s ease;
}
.horizontal_divider:hover {
background-color: var(--color-secondary);
height: 3px;
}
/* Messages Quadrant - Remove old styles that conflict */
.messages_quadrant .quadrant_content {
padding: 0;
}
/* Override for MessageList component */
.messages_quadrant :global(.chat_messages) {
border-radius: 0;
margin-bottom: 0;
height: 100%;
padding: 15px;
}
/* Input Quadrant - Remove old styles that conflict */
.input_quadrant .quadrant_content {
padding: 15px;
}
.chat_messages {
flex: 1;
padding: 15px;
border-radius: 15px;
margin-bottom: 15px;
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
scroll-behavior: smooth;
}
.chat_messages::-webkit-scrollbar {
width: 6px;
}
.chat_messages::-webkit-scrollbar-track {
background: transparent;
}
.chat_messages::-webkit-scrollbar-thumb {
background: var(--color-gray-disabled);
border-radius: 3px;
}
.chat_messages::-webkit-scrollbar-thumb:hover {
background: var(--color-gray);
}
.messages_container {
min-height: 100%;
display: flex;
flex-direction: column;
justify-content: flex-start;
}
.messages_spacer {
flex: 1;
min-height: 20px;
}
.chat_input {
display: flex;
gap: 10px;
align-items: flex-end;
flex-shrink: 0;
flex-direction: column;
transition: all 0.2s ease;
}
.chat_input.drag_over {
background-color: var(--color-secondary-disabled);
border: 2px dashed var(--color-secondary);
border-radius: 12px;
padding: 8px;
}
.input_row {
display: flex;
gap: 10px;
align-items: flex-end;
width: 100%;
}
.message_input {
flex: 1;
padding: 12px 16px;
border: 1px solid var(--color-gray-disabled);
border-radius: 12px;
outline: none;
font-size: 14px;
font-family: var(--font-family);
background-color: var(--color-bg);
color: var(--color-text);
}
.message_input:focus {
border-color: var(--color-secondary);
}
.message_input:disabled {
background-color: var(--color-surface);
cursor: not-allowed;
opacity: 0.6;
}
.attachment_button {
height: 48px;
width: 48px;
background-color: var(--color-secondary-disabled);
color: var(--color-secondary);
border: none;
border-radius: 12px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.2s ease, border-color 0.2s ease;
font-family: var(--font-family);
}
.attachment_button:hover {
background-color: var(--color-secondary-hover);
color: var(--color-bg);
}
.attachment_button:disabled {
background-color: var(--color-surface);
cursor: not-allowed;
opacity: 0.6;
}
.attached_files {
display: flex;
flex-wrap: wrap;
gap: 8px;
width: 100%;
margin-bottom: 5px;
}
.attached_file {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 8px;
background-color: var(--color-secondary-disabled);
border: 1px solid var(--color-secondary);
border-radius: 8px;
font-size: 12px;
color: var(--color-secondary);
font-family: var(--font-family);
}
.attached_file_icon {
font-size: 16px;
}
.attached_file_name {
max-width: 150px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.attached_file_remove {
background: none;
border: none;
color: var(--color-gray);
cursor: pointer;
padding: 0;
margin-left: 4px;
display: flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
border-radius: 50%;
transition: background-color 0.2s ease, color 0.2s ease;
}
.attached_file_remove:hover {
background-color: var(--color-gray-disabled);
color: var(--color-text);
}
.send_button {
height: 48px;
width: 48px;
background-color: var(--color-secondary);
color: var(--color-bg);
border: 1px solid var(--color-secondary);
border-radius: 12px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
display: flex;
align-items: center;
justify-content: center;
font-family: var(--font-family);
}
.send_button:hover {
background-color: var(--color-secondary-hover);
}
.send_button_icon {
height: 60%;
width: 60%;
margin: none;
padding: none;
}
.send_button:disabled {
background-color: var(--color-gray-disabled);
cursor: not-allowed;
opacity: 0.6;
border: 1px solid var(--color-gray-disabled);
}
.stop_button {
padding: 12px 12px;
height: 48px;
width: 48px;
background-color: var(--color-red);
color: var(--color-bg);
border: none;
border-radius: 12px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
display: flex;
align-items: center;
justify-content: center;
font-family: var(--font-family);
}
.stop_button:hover {
background-color: var(--color-red-hover);
}
.stop_button:disabled {
background-color: var(--color-gray-disabled);
cursor: not-allowed;
opacity: 0.6;
}
.loading_message {
padding: 10px;
background-color: var(--color-secondary-disabled);
border-left: 4px solid var(--color-secondary);
border-radius: 4px;
margin-bottom: 10px;
}
.loading_message p {
margin: 0;
color: var(--color-secondary);
font-size: 14px;
font-family: var(--font-family);
}
.error_message {
padding: 10px;
background-color: var(--color-red-disabled);
border-left: 4px solid var(--color-red);
border-radius: 4px;
margin-bottom: 10px;
}
.error_message p {
margin: 0;
color: var(--color-red);
font-size: 14px;
font-family: var(--font-family);
}
.message {
margin-bottom: 15px;
padding: 12px;
border-radius: 12px;
max-width: 80%;
font-family: var(--font-family);
}
.message_user {
background-color: var(--color-secondary);
color: var(--color-bg);
margin-left: auto;
margin-right: 0;
}
.message_assistant {
background-color: var(--color-surface);
color: var(--color-text);
margin-left: 0;
margin-right: auto;
}
.message_system {
background-color: var(--color-primary-disabled);
color: var(--color-primary);
margin-left: auto;
margin-right: auto;
text-align: center;
}
.message_role {
font-size: 12px;
font-weight: 600;
margin-bottom: 4px;
opacity: 0.8;
font-family: var(--font-family);
}
.message_content {
font-size: 14px;
line-height: 1.4;
white-space: pre-wrap;
word-wrap: break-word;
font-family: var(--font-family);
}
.message_timestamp {
font-size: 11px;
margin-top: 4px;
opacity: 0.6;
font-family: var(--font-family);
}
.placeholder_text {
text-align: center;
color: var(--color-gray);
font-style: italic;
margin: 20px 0;
font-family: var(--font-family);
}
.workflow_status {
display: flex;
justify-content: center;
align-items: center;
padding: 10px;
margin: 5px 0;
}
.workflow_status p {
margin: 0;
font-size: 14px;
color: var(--color-gray);
font-family: var(--font-family);
}
.retry_container {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
background-color: var(--color-red-disabled);
border: 1px solid var(--color-red);
border-radius: 12px;
}
.failed_message {
font-size: 14px;
color: var(--color-red);
font-family: var(--font-family);
font-weight: 500;
}
.retry_button {
background-color: var(--color-primary);
color: var(--color-bg);
border: none;
border-radius: 8px;
padding: 8px 16px;
font-size: 12px;
font-weight: 500;
cursor: pointer;
font-family: var(--font-family);
transition: background-color 0.2s ease, transform 0.2s ease;
white-space: nowrap;
}
.retry_button:hover {
background-color: var(--color-primary-hover);
transform: translateY(-1px);
}
.retry_button:disabled {
background-color: var(--color-gray-disabled);
cursor: not-allowed;
transform: none;
opacity: 0.6;
}
.completion_message {
padding: 10px 12px;
background-color: var(--color-secondary-disabled);
border-left: 4px solid var(--color-secondary);
border-radius: 4px;
margin-bottom: 10px;
text-align: center;
}
.completion_message p {
margin: 0 0 10px 0;
color: var(--color-secondary);
font-size: 14px;
font-weight: 600;
font-family: var(--font-family);
}
.new_workflow_button {
background-color: var(--color-secondary);
color: var(--color-bg);
border: none;
border-radius: 8px;
padding: 8px 16px;
font-size: 12px;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s ease;
font-family: var(--font-family);
}
.new_workflow_button:hover {
background-color: var(--color-secondary-hover);
}
.message_documents {
margin-top: 8px;
display: flex;
flex-direction: column;
gap: 6px;
}
.document_item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 10px;
background-color: var(--color-bg);
border: 1px solid var(--color-gray-disabled);
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease;
font-size: 13px;
}
.document_item:hover {
border-color: var(--color-primary);
background-color: var(--color-surface);
}
.message_assistant .document_item {
background-color: var(--color-bg);
border-color: var(--color-gray-disabled);
}
.message_assistant .document_item:hover {
border-color: var(--color-primary);
background-color: var(--color-surface);
}
.document_icon {
font-size: 16px;
color: var(--color-secondary);
flex-shrink: 0;
}
.document_info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 2px;
}
.document_name {
font-weight: 500;
color: var(--color-text);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-family: var(--font-family);
}
.document_meta {
display: flex;
gap: 8px;
font-size: 11px;
color: var(--color-gray);
font-family: var(--font-family);
}
.document_size {
font-size: 11px;
color: var(--color-gray);
}
.document_type {
font-size: 11px;
color: var(--color-gray);
}
.document_actions {
display: flex;
gap: 4px;
opacity: 1;
transition: opacity 0.2s ease;
}
.document_item:hover .document_actions {
opacity: 1;
}
.document_action_button {
background: none;
border: none;
color: var(--color-gray);
cursor: pointer;
padding: 4px;
border-radius: 4px;
font-size: 14px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.document_action_button:hover {
background-color: var(--color-surface);
color: var(--color-text);
}
.message_assistant .document_action_button {
color: var(--color-gray);
}
.message_assistant .document_action_button:hover {
background-color: var(--color-surface);
color: var(--color-text);
}
/* File Preview Quadrant Styles */
.empty_state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
text-align: center;
color: var(--color-gray);
}
.empty_state small {
margin-top: 8px;
font-size: 12px;
opacity: 0.7;
}
.loading_state {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: var(--color-gray);
}
.file_preview {
display: flex;
flex-direction: column;
gap: 15px;
height: 100%;
}
.file_info h4 {
margin: 0 0 8px 0;
font-size: 16px;
font-weight: 500;
color: var(--color-text);
}
.file_metadata {
display: flex;
gap: 15px;
font-size: 12px;
color: var(--color-gray);
}
.preview_modes {
display: flex;
gap: 8px;
border-bottom: 1px solid var(--color-gray-disabled);
padding-bottom: 8px;
}
.mode_button {
padding: 6px 12px;
border: 1px solid var(--color-gray-disabled);
background: var(--color-bg);
color: var(--color-text);
border-radius: 6px;
cursor: pointer;
font-size: 12px;
transition: all 0.2s ease;
}
.mode_button:hover {
background: var(--color-surface);
}
.mode_button.active {
background: var(--color-secondary);
color: white;
border-color: var(--color-secondary);
}
.preview_content {
flex: 1;
overflow: auto;
}
.image_preview {
max-width: 100%;
max-height: 100%;
object-fit: contain;
border-radius: 8px;
}
.text_preview {
background: var(--color-surface);
border-radius: 8px;
padding: 15px;
font-family: 'Courier New', monospace;
font-size: 12px;
line-height: 1.4;
overflow: auto;
}
.document_preview {
display: flex;
flex-direction: column;
align-items: center;
gap: 15px;
padding: 20px;
}
.download_link {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
background: var(--color-secondary);
color: white;
text-decoration: none;
border-radius: 6px;
font-size: 14px;
transition: background-color 0.2s ease;
}
.download_link:hover {
background: var(--color-primary);
}
/* Connected Files Quadrant Styles */
.files_count {
font-size: 12px;
color: var(--color-gray);
margin-left: auto;
}
.files_list {
display: flex;
flex-direction: column;
gap: 8px;
}
.file_item {
display: flex;
align-items: center;
gap: 12px;
padding: 10px;
border: 1px solid var(--color-gray-disabled);
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
background: var(--color-bg);
}
.file_item:hover {
background: var(--color-surface);
border-color: var(--color-secondary);
}
.file_item.selected {
background: var(--color-secondary-disabled);
border-color: var(--color-secondary);
}
.file_icon {
font-size: 20px;
flex-shrink: 0;
}
.file_details {
flex: 1;
min-width: 0;
}
.file_name {
font-size: 14px;
font-weight: 500;
color: var(--color-text);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.file_meta {
font-size: 12px;
color: var(--color-gray);
margin-top: 2px;
}
.file_actions {
display: flex;
gap: 4px;
}
.action_button {
width: 28px;
height: 28px;
border: none;
background: transparent;
cursor: pointer;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.2s ease;
font-size: 14px;
}
.action_button:hover {
background: var(--color-gray-disabled);
}

View file

@ -0,0 +1,137 @@
/* Input Area Specific Styles */
.input_area_container {
width: 100%;
display: flex;
flex-direction: column;
justify-content: flex-end;
box-sizing: border-box;
padding: 16px 16px 16px 0;
min-height: fit-content;
}
.workflow_status {
margin-bottom: 12px;
padding: 8px;
background-color: var(--color-surface);
border-radius: 4px;
font-size: 12px;
color: var(--color-gray);
}
.error_message {
padding: 8px;
background-color: var(--color-error, #ffe6e6);
color: var(--color-error-text, #d00);
border-radius: 4px;
margin-bottom: 12px;
}
.attached_files_count {
margin-bottom: 8px;
padding: 6px 10px;
background-color: var(--color-secondary-disabled);
border-radius: 25px;
font-size: 12px;
color: var(--color-bg);
text-align: center;
}
.input_form_container {
display: flex;
flex-direction: column;
gap: 12px;
}
.floating_label_textarea {
position: relative;
width: 100%;
box-sizing: border-box;
}
.textarea_label {
position: absolute;
left: 16px;
top: 16px;
color: var(--color-text);
opacity: 0.6;
font-size: 14px;
pointer-events: none;
transition: all 0.3s ease;
background-color: transparent;
font-family: var(--font-family);
z-index: 1;
}
.textarea_label_focused {
position: absolute;
left: 12px;
top: -8px;
transform: translateY(0);
color: var(--color-secondary);
font-size: 12px;
pointer-events: none;
transition: all 0.3s ease;
background-color: var(--color-bg);
padding: 0 4px;
font-family: var(--font-family);
font-weight: 500;
z-index: 2;
}
.message_textarea {
resize: none;
width: 100%;
min-height: calc(1.5em * 4 + 32px); /* 4 rows + padding */
max-height: calc(1.5em * 8 + 32px); /* 8 rows + padding */
height: calc(1.5em * 4 + 32px); /* Start with 4 rows */
padding:16px;
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.6;
transition: border-color 0.2s ease, opacity 0.2s ease, box-shadow 0.2s ease;
box-sizing: border-box;
line-height: 1.5;
overflow-y: auto;
}
.message_textarea:focus {
outline: none;
border-color: var(--color-secondary);
opacity: 1;
box-shadow: 0 0 0 2px rgba(var(--color-secondary), 0.1);
}
.message_textarea::placeholder {
color: transparent;
}
.message_textarea:disabled {
opacity: 0.6;
}
.input_actions_row {
display: flex;
gap: 8px;
align-items: center;
}
.new_chat_button {
padding: 8px 12px;
background-color: var(--color-surface);
border: 1px solid var(--color-gray-disabled);
border-radius: 6px;
cursor: pointer;
font-size: 12px;
color: var(--color-gray);
}
.prompt_indicator {
font-size: 12px;
color: var(--color-gray);
}

View file

@ -1,68 +0,0 @@
.chat-grid {
display: grid;
width: 100%;
height: 100%;
gap: 0;
overflow: hidden;
}
.quadrant {
overflow: hidden;
background-color: var(--color-bg);
border: 1px solid var(--color-gray-disabled);
display: flex;
flex-direction: column;
min-height: 0;
min-width: 0;
}
.divider {
background-color: var(--color-primary);
cursor: ns-resize;
transition: background-color 0.2s ease;
}
.vertical-divider {
cursor: ew-resize;
grid-row: 1 / -1;
grid-column: 2;
}
.horizontal-divider {
cursor: ns-resize;
grid-row: 2;
grid-column: 1 / -1;
}
.divider:hover {
background-color: var(--color-secondary);
}
/* Quadrant specific styles */
.messages-quadrant {
grid-row: 1;
grid-column: 1;
border-right: none;
border-bottom: none;
background-color: #e8f5e8 !important; /* Light green */
}
.file-preview-quadrant {
grid-row: 1;
grid-column: 3;
border-bottom: none;
background-color: #ffe8e8 !important; /* Light red */
}
.input-quadrant {
grid-row: 3;
grid-column: 1;
border-right: none;
background-color: #f0e8ff !important; /* Light purple */
}
.connected-files-quadrant {
grid-row: 3;
grid-column: 3;
background-color: #e8f0ff !important; /* Light blue */
}

View file

@ -1,109 +0,0 @@
# DashboardChatArea - Modular Structure
This directory contains the refactored `DashboardChatArea` component, broken down into manageable modules for better maintainability and separation of concerns.
## File Structure
```
DashboardChatArea/
├── index.ts # Main export file
├── types.ts # TypeScript interfaces and types
├── DashboardChatArea.tsx # Main orchestrating component
├── useChatLogic.ts # Custom hook with all business logic
├── MessageList.tsx # Component for displaying messages
├── MessageItem.tsx # Individual message component
├── ChatInput.tsx # Input field and send button component
├── WorkflowStatusDisplay.tsx # Workflow status and completion UI
├── DashboardChatArea.module.css # Shared styles
└── README.md # This documentation
```
## Component Responsibilities
### `DashboardChatArea.tsx` (Main Component)
- **Purpose**: Orchestrates all child components
- **Responsibilities**:
- Uses the `useChatLogic` hook
- Renders `MessageList` and `ChatInput` components
- Passes props between components
- **Size**: ~73 lines (reduced from 278 lines)
### `useChatLogic.ts` (Custom Hook)
- **Purpose**: Contains all business logic and state management
- **Responsibilities**:
- State management (input value, workflow ID, completion status)
- Effects for polling, auto-scroll, prompt handling
- Workflow operations (send messages, start workflows)
- Event handlers
- **Size**: ~196 lines
### `MessageList.tsx` (Message Display)
- **Purpose**: Handles the display of all messages and status indicators
- **Responsibilities**:
- Renders loading and error states
- Maps through messages using `MessageItem`
- Includes `WorkflowStatusDisplay`
- Handles auto-scroll reference
- **Size**: ~73 lines
### `MessageItem.tsx` (Individual Message)
- **Purpose**: Renders a single message
- **Responsibilities**:
- Message content display
- Role-based styling
- Timestamp formatting
- **Size**: ~32 lines
### `ChatInput.tsx` (Input Interface)
- **Purpose**: Handles user input and send functionality
- **Responsibilities**:
- Input field with ref handling
- Send button with animations
- Keyboard event handling
- Disabled states
- **Size**: ~46 lines
### `WorkflowStatusDisplay.tsx` (Status UI)
- **Purpose**: Shows workflow status and completion states
- **Responsibilities**:
- Running workflow status
- Completion message
- "Start New Workflow" button
- **Size**: ~38 lines
### `types.ts` (Type Definitions)
- **Purpose**: Centralized TypeScript interfaces
- **Responsibilities**:
- Component prop interfaces
- Data structure types
- Shared type definitions
- **Size**: ~50 lines
## Benefits of This Structure
1. **Separation of Concerns**: Each file has a single, clear responsibility
2. **Reusability**: Components can be easily reused or tested independently
3. **Maintainability**: Easier to locate and modify specific functionality
4. **Readability**: Smaller files are easier to understand and navigate
5. **Testing**: Individual components can be unit tested in isolation
6. **Type Safety**: Centralized types ensure consistency across components
## Usage
Import the main component as before:
```typescript
import DashboardChatArea from './DashboardChatArea';
// or
import DashboardChatArea, { DashboardChatAreaProps } from './DashboardChatArea';
```
The API remains exactly the same - this refactoring is purely internal and doesn't affect how the component is used by parent components.
## Development Guidelines
- **Adding new features**: Consider which component/file is most appropriate
- **State changes**: Most state logic should go in `useChatLogic.ts`
- **UI changes**: Modify the relevant component file
- **New types**: Add to `types.ts`
- **Styling**: All styles remain in `DashboardChatArea.module.css`

View file

@ -0,0 +1,194 @@
import { useState, useEffect, useCallback, useRef } from 'react';
import { useWorkflow, useWorkflowMessages, useWorkflowOperations, StartWorkflowRequest } from '../../../hooks/useWorkflows';
export interface WorkflowManagerState {
currentWorkflowId: string | null;
workflow: any;
messages: any[];
isLoading: boolean;
error: string | null;
isPolling: boolean;
}
export interface WorkflowManagerActions {
loadWorkflow: (workflowId: string) => Promise<void>;
startNewWorkflow: (prompt: string, fileIds: number[]) => Promise<string | null>;
continueWorkflow: (prompt: string, fileIds: number[]) => Promise<boolean>;
clearWorkflow: () => void;
refreshMessages: () => Promise<void>;
setPolling: (enabled: boolean) => void;
}
export function useWorkflowManager(initialWorkflowId?: string | null): [WorkflowManagerState, WorkflowManagerActions] {
// Core state
const [currentWorkflowId, setCurrentWorkflowId] = useState<string | null>(initialWorkflowId || null);
const [isPolling, setIsPolling] = useState(false);
const pollingIntervalRef = useRef<number | null>(null);
// Hook-based data fetching
const { workflow, loading: workflowLoading, error: workflowError, refetch: refetchWorkflow } = useWorkflow(currentWorkflowId);
const { messages, loading: messagesLoading, error: messagesError, refetch: refetchMessages } = useWorkflowMessages(currentWorkflowId);
const { startWorkflow } = useWorkflowOperations();
// Combined loading and error states
const isLoading = workflowLoading || messagesLoading;
const error = workflowError || messagesError;
// Auto-polling for active workflows and message updates
useEffect(() => {
if (isPolling && currentWorkflowId) {
console.log(`🔄 Starting auto-polling for workflow: ${currentWorkflowId}`);
pollingIntervalRef.current = window.setInterval(() => {
console.log('🔄 Auto-polling workflow and messages...');
// Always poll for messages when workflow is active
refetchMessages();
// Also poll workflow status if we have workflow data
if (workflow) {
const isActive = ['running', 'processing', 'started'].includes(workflow.status);
if (isActive) {
refetchWorkflow();
}
}
}, 2000); // Poll every 2 seconds for smoother updates
}
return () => {
if (pollingIntervalRef.current) {
console.log('🛑 Stopping auto-polling');
clearInterval(pollingIntervalRef.current);
pollingIntervalRef.current = null;
}
};
}, [isPolling, currentWorkflowId, workflow?.status, refetchWorkflow, refetchMessages]);
// Actions
const loadWorkflow = useCallback(async (workflowId: string) => {
console.log(`📂 Loading workflow: ${workflowId}`);
setCurrentWorkflowId(workflowId);
// The hooks will automatically fetch data when workflowId changes
}, []);
const startNewWorkflow = useCallback(async (prompt: string, fileIds: number[] = []): Promise<string | null> => {
console.log('🚀 Starting new workflow with prompt:', prompt.substring(0, 50) + '...');
const workflowData: StartWorkflowRequest = {
prompt,
listFileId: fileIds
};
const result = await startWorkflow(workflowData);
if (result.success && result.data) {
const newWorkflowId = result.data.id;
console.log(`✅ New workflow started: ${newWorkflowId}`);
setCurrentWorkflowId(newWorkflowId);
setIsPolling(true); // Start polling immediately for new workflows
// Also immediately fetch messages to get the user's message
setTimeout(() => {
refetchMessages();
}, 500);
return newWorkflowId;
} else {
console.error('❌ Failed to start workflow:', result.error);
return null;
}
}, [startWorkflow, refetchMessages]);
const continueWorkflow = useCallback(async (prompt: string, fileIds: number[] = []): Promise<boolean> => {
if (!currentWorkflowId) {
console.error('❌ Cannot continue workflow: no current workflow ID');
return false;
}
console.log(`➡️ Continuing workflow ${currentWorkflowId} with prompt:`, prompt.substring(0, 50) + '...');
const workflowData: StartWorkflowRequest = {
prompt,
listFileId: fileIds
};
const result = await startWorkflow(workflowData, currentWorkflowId);
if (result.success) {
console.log(`✅ Workflow ${currentWorkflowId} continued`);
setIsPolling(true); // Ensure polling is enabled
// Immediately start polling for new messages
setTimeout(() => {
refetchMessages();
}, 500);
return true;
} else {
console.error('❌ Failed to continue workflow:', result.error);
return false;
}
}, [currentWorkflowId, startWorkflow, refetchMessages]);
const clearWorkflow = useCallback(() => {
console.log('🧹 Clearing workflow');
setCurrentWorkflowId(null);
setIsPolling(false);
}, []);
const refreshMessages = useCallback(async () => {
console.log('🔄 Manually refreshing messages');
await refetchMessages();
}, [refetchMessages]);
const setPollingEnabled = useCallback((enabled: boolean) => {
console.log(`🔄 Setting polling to: ${enabled}`);
setIsPolling(enabled);
}, []);
// Initialize workflow on mount if provided
useEffect(() => {
if (initialWorkflowId && initialWorkflowId !== currentWorkflowId) {
loadWorkflow(initialWorkflowId);
}
}, [initialWorkflowId, currentWorkflowId, loadWorkflow]);
// Auto-enable polling when workflow becomes active, keep polling until completed
useEffect(() => {
if (currentWorkflowId && workflow) {
const isActive = ['running', 'processing', 'started'].includes(workflow.status);
const isCompleted = ['completed', 'failed', 'stopped'].includes(workflow.status);
if (isActive) {
console.log(`🟢 Workflow ${currentWorkflowId} is active (${workflow.status}), enabling polling`);
setIsPolling(true);
} else if (isCompleted) {
console.log(`🔴 Workflow ${currentWorkflowId} is completed (${workflow.status}), disabling polling`);
setIsPolling(false);
}
} else if (currentWorkflowId && !workflow) {
// If we have a workflow ID but no workflow data yet, start polling to get updates
console.log(`⏳ Workflow ${currentWorkflowId} loaded, starting polling for updates`);
setIsPolling(true);
}
}, [currentWorkflowId, workflow?.status]);
const state: WorkflowManagerState = {
currentWorkflowId,
workflow,
messages,
isLoading,
error,
isPolling
};
const actions: WorkflowManagerActions = {
loadWorkflow,
startNewWorkflow,
continueWorkflow,
clearWorkflow,
refreshMessages,
setPolling: setPollingEnabled
};
return [state, actions];
}

View file

@ -1,338 +0,0 @@
.dashboard_log {
display: flex;
padding: 20px;
flex-direction: column;
align-self: stretch;
border-radius: 30px;
background: var(--color-bg);
position: relative;
box-shadow: 0px 2px 6px 0px rgba(194, 194, 194, 0.10);
height: 100%;
width: 100%;
min-height: 0;
overflow: hidden;
}
.dashboard_log.expanded {
width: 100%;
}
.log_header {
display: flex;
justify-content: space-between;
align-items: flex-start;
width: 100%;
flex-shrink: 0;
}
.log_title_div {
display: flex;
flex-direction: column;
}
.log_title {
text-align: center;
font-size: 14px;
font-style: normal;
font-weight: 500;
line-height: normal;
border: none;
background: none;
outline: none;
color: var(--color-text);
cursor: default;
}
.log_title_collapsed {
opacity: 50%;
color: var(--color-gray);
}
.collapseIcon {
cursor: pointer;
display: flex;
align-items: center;
color: var(--color-gray);
}
.collapseIcon:hover {
color: var(--color-gray);
}
.horizontalLine {
width: 100%;
height: 2px;
margin-top: 19px;
}
.horizontalLineLight {
width: calc(100%);
background-color: var(--color-gray-disabled);
height: 2px;
margin-top: 39px;
margin-left: -20px;
position: absolute;
flex-shrink: 0;
}
.log_content {
margin-top: 20px;
flex: 1;
min-height: 0;
overflow: hidden;
display: flex;
flex-direction: column;
}
.log_entries {
display: flex;
flex-direction: column;
gap: 4px;
scroll-behavior: smooth;
}
.log_entries::-webkit-scrollbar {
display: none;
}
.log_entry {
display: flex;
align-items: flex-start;
gap: 8px;
padding: 4px 0;
animation: fadeIn 0.3s ease-in;
}
.log_entry:last-child {
border-bottom: none;
}
.log_timestamp {
color: #888;
font-size: 11px;
min-width: 80px;
font-weight: bold;
}
.log_level {
color: #00aaff;
font-size: 11px;
font-weight: bold;
min-width: 60px;
}
.log_message {
color: #00ff00;
font-size: 12px;
flex: 1;
word-break: break-word;
line-height: 1.3;
}
.log_level_info {
background-color: #4CAF50;
color: white;
padding: 2px 8px;
border-radius: 30px;
font-size: 10px;
font-weight: bold;
min-width: 45px;
text-align: center;
}
.log_level_warning {
background-color: #FF9800;
color: white;
padding: 2px 8px;
border-radius: 30px;
font-size: 10px;
font-weight: bold;
min-width: 45px;
text-align: center;
}
.log_level_error {
background-color: #F44336;
color: white;
padding: 2px 8px;
border-radius: 12px;
font-size: 10px;
font-weight: bold;
min-width: 45px;
text-align: center;
}
/* Hacker-style console styles */
.console_container {
background-color: #0a0a0a;
border-radius: 15px;
height: 100%;
padding: 15px;
flex: 1;
min-height: 0;
max-height: 100%;
overflow-y: auto;
font-family: 'Courier New', 'Monaco', 'Menlo', monospace;
font-size: 12px;
line-height: 1.4;
color: #00ff00;
border: 1px solid #333;
box-shadow: inset 0 0 10px rgba(0, 255, 0, 0.1);
display: flex;
flex-direction: column;
}
.console_container::-webkit-scrollbar {
width: 8px;
}
.console_container::-webkit-scrollbar-track {
background: #1a1a1a;
border-radius: 4px;
}
.console_container::-webkit-scrollbar-thumb {
background: #333;
border-radius: 4px;
}
.console_container::-webkit-scrollbar-thumb:hover {
background: #555;
}
.console_content {
flex: 1;
display: flex;
flex-direction: column;
gap: 2px;
}
.console_line {
display: flex;
flex-wrap: wrap;
margin-bottom: 2px;
animation: fadeIn 0.3s ease-in;
}
.console_timestamp {
color: #888;
margin-right: 8px;
font-weight: bold;
min-width: 80px;
}
.console_level {
margin-right: 8px;
font-weight: bold;
min-width: 60px;
}
.console_message {
color: #00ff00;
flex: 1;
word-break: break-word;
}
.console_data {
width: 100%;
margin-top: 4px;
margin-left: 20px;
color: #00aaff;
font-size: 11px;
white-space: pre-wrap;
background-color: rgba(0, 170, 255, 0.1);
padding: 8px;
border-radius: 4px;
border-left: 3px solid #00aaff;
}
.console_prompt {
color: #00ff00;
margin-right: 8px;
font-weight: bold;
}
.console_text {
color: #00ff00;
}
.console_placeholder {
display: flex;
align-items: center;
opacity: 0.7;
font-style: italic;
padding: 10px 0;
min-height: 30px;
}
.console_loading {
display: flex;
align-items: center;
padding: 10px 0;
min-height: 30px;
}
.console_cursor {
color: #00ff00;
animation: blink 1s infinite;
margin-left: 4px;
}
.console_error {
display: flex;
align-items: center;
padding: 10px 0;
min-height: 30px;
}
.console_error .console_text {
color: #ff4444;
}
.console_empty {
display: flex;
align-items: center;
opacity: 0.7;
font-style: italic;
padding: 10px 0;
min-height: 30px;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(5px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes blink {
0%, 50% {
opacity: 1;
}
51%, 100% {
opacity: 0;
}
}
/* Live indicator styling */
.live_indicator {
color: #00ff00;
font-size: 12px;
font-weight: bold;
animation: pulse 1.5s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.7;
transform: scale(1.05);
}
}

View file

@ -1,215 +0,0 @@
import React, { useState, useEffect, useRef } from "react";
import { motion } from "framer-motion";
import styles from './DashboardLog.module.css';
import { useApiRequest } from '../../../hooks/useApi';
import { useLanguage } from '../../../contexts/LanguageContext';
interface DashboardLogProps {
isExpanded: boolean;
workflowId: string | null;
workflowCompleted?: boolean;
}
const DashboardLog: React.FC<DashboardLogProps> = ({ isExpanded, workflowId, workflowCompleted = false }) => {
const { t } = useLanguage();
const [logs, setLogs] = useState<any[]>([]);
const [isPolling, setIsPolling] = useState(false);
const [logsError, setLogsError] = useState<string | null>(null);
const intervalRef = useRef<number | null>(null);
const consoleContainerRef = useRef<HTMLDivElement>(null);
const { request, isLoading: logsLoading } = useApiRequest<null, any[]>();
// Function to fetch logs directly
const fetchLogs = async (workflowIdToFetch: string) => {
try {
console.log('Fetching logs for workflow:', workflowIdToFetch);
const data = await request({
url: `/api/workflows/${workflowIdToFetch}/logs`,
method: 'get'
});
console.log('Logs fetched:', data);
setLogs(data || []);
setLogsError(null);
} catch (error: any) {
console.error('Error fetching logs:', error);
setLogsError(error.message || t('dashboard.log.fetch_failed'));
}
};
// Auto-scroll to bottom when new logs arrive during polling
useEffect(() => {
if (isPolling && consoleContainerRef.current && logs.length > 0) {
consoleContainerRef.current.scrollTop = consoleContainerRef.current.scrollHeight;
}
}, [logs, isPolling]);
// Start/stop log polling based on workflow state
useEffect(() => {
console.log('Log polling effect triggered:', { workflowId, workflowCompleted, isPolling });
// Clear any existing interval
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
if (!workflowId) {
// No workflow - stop everything
console.log('No workflow ID, stopping polling');
setIsPolling(false);
setLogs([]);
setLogsError(null);
return;
}
if (workflowCompleted) {
// Workflow completed - stop polling but fetch final logs
console.log('Workflow completed, stopping polling but fetching final logs');
setIsPolling(false);
fetchLogs(workflowId);
} else {
// Workflow is running - start polling immediately
console.log('Workflow running, starting polling');
setIsPolling(true);
// Fetch logs immediately
fetchLogs(workflowId);
// Start polling every second
intervalRef.current = setInterval(() => {
console.log('Polling for logs...');
fetchLogs(workflowId);
}, 1000);
}
// Cleanup on unmount or dependency change
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
};
}, [workflowId, workflowCompleted]);
// Debug logs data
useEffect(() => {
console.log('Logs data updated:', {
logsCount: logs.length,
logs: logs.slice(0, 3), // Show first 3 logs
logsLoading,
logsError,
isPolling
});
}, [logs, logsLoading, logsError, isPolling]);
const renderLogContent = () => {
if (!workflowId) {
return (
<div className={styles.console_placeholder}>
<span className={styles.console_prompt}>$</span>
<span className={styles.console_text}>{t('dashboard.log.no_workflow')}</span>
</div>
);
}
if (logsLoading && logs.length === 0) {
return (
<div className={styles.console_placeholder}>
<span className={styles.console_prompt}>$</span>
<span className={styles.console_text}>{t('dashboard.log.loading')}</span>
</div>
);
}
if (logsError) {
return (
<div className={styles.console_placeholder}>
<span className={styles.console_prompt}>$</span>
<span className={styles.console_text}>{t('dashboard.log.error')}: {logsError}</span>
</div>
);
}
if (logs.length === 0) {
const statusText = workflowCompleted
? t('dashboard.log.no_logs')
: t('dashboard.log.waiting');
return (
<div className={styles.console_placeholder}>
<span className={styles.console_prompt}>$</span>
<span className={styles.console_text}>{statusText}</span>
{isPolling && <span className={styles.console_cursor}>|</span>}
</div>
);
}
return (
<div className={styles.log_entries}>
{logs.map((log, index) => (
<div key={log.id || index} className={styles.log_entry}>
<span className={styles.log_timestamp}>
{log.timestamp ? new Date(log.timestamp).toLocaleTimeString() : ''}
</span>
<span className={styles.log_level}>[{log.level || t('dashboard.log.level.info')}]</span>
<span className={styles.log_message}>{log.message || log.content || JSON.stringify(log)}</span>
</div>
))}
</div>
);
};
return (
<motion.div
className={`${styles.dashboard_log} ${isExpanded ? styles.expanded : ''}`}
layout
transition={{ duration: 0.4, ease: "easeOut" }}
>
<motion.div
className={styles.log_header}
layout
transition={{ duration: 0.3, ease: "easeOut" }}
>
<div className={styles.log_title_div}>
<motion.div
className={styles.log_title}
whileHover={{ scale: 1.05 }}
transition={{ duration: 0.2, ease: "easeOut" }}
>
{t('dashboard.log.title')} {workflowId && `- ${t('dashboard.log.workflow')} ${workflowId.substring(0, 8)}...`}
</motion.div>
<motion.div
className={styles.horizontalLine}
initial={{ opacity: 0, width: "0%" }}
animate={{ opacity: 1, width: "100%" }}
transition={{ duration: 0.3, ease: "easeOut" }}
></motion.div>
</div>
</motion.div>
<motion.div
className={styles.horizontalLineLight}
initial={{ opacity: 0, scaleX: 0 }}
animate={{ opacity: 1, scaleX: 1 }}
transition={{ duration: 0.3, ease: "easeOut" }}
style={{ transformOrigin: "left" }}
></motion.div>
<motion.div
className={styles.log_content}
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: "auto" }}
transition={{ duration: 0.4, ease: "easeOut" }}
>
<div className={styles.console_container} ref={consoleContainerRef}>
<div className={styles.console_content}>
{renderLogContent()}
</div>
</div>
</motion.div>
</motion.div>
);
};
export default DashboardLog;

View file

@ -1,157 +0,0 @@
.dashboard_prompt {
display: flex;
padding: 20px;
flex-direction: column;
align-self: stretch;
border-radius: 30px;
background: var(--color-bg);
position: relative;
box-shadow: 0px 2px 6px 0px rgba(194, 194, 194, 0.10);
width: 100%;
transition: height 0.3s ease;
font-family: var(--font-family);
}
.dashboard_prompt:not(.collapsed) {
height: 100%;
overflow: hidden;
display: flex;
flex-direction: column;
}
.dashboard_prompt.collapsed {
height: auto;
min-height: auto;
}
.prompt_header {
display: flex;
justify-content: space-between;
align-items: flex-start;
width: 100%;
}
.prompt_button_div {
display: flex;
align-self: stretch;
gap: 30px;
}
.prompt_button {
text-align: center;
font-size: 14px;
font-style: normal;
font-weight: 500;
line-height: normal;
border: none;
background: none;
outline: none;
color: var(--color-text);
transition: opacity 0.3s ease, color 0.3s ease;
font-family: var(--font-family);
}
.prompt_button_inactive {
opacity: 50%;
}
.prompt_button_collapsed {
opacity: 50%;
color: var(--color-gray);
}
.buttonWrapper {
display: flex;
flex-direction: column;
}
.expandIcon {
cursor: pointer;
display: flex;
align-items: center;
color: var(--color-gray);
transition: color 0.3s ease;
}
.expandIcon:hover {
color: var(--color-text);
}
.horizontalLine {
width: 100%;
background-color: var(--color-text);
height: 2px;
margin-top: 19px;
transition: opacity 0.3s ease;
}
.horizontalLineLight {
width: calc(100%);
background-color: var(--color-gray-disabled);
height: 2px;
margin-top: 39px;
margin-left: -20px;
position: absolute;
transition: opacity 0.3s ease;
}
.content_wrapper {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
position: relative;
}
.content_area {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
transition: all 0.3s ease;
opacity: 1;
position: relative;
}
.content_collapsed {
opacity: 0;
max-height: 0;
overflow: hidden;
padding: 0;
margin: 0;
}
.scrollableContent {
flex: 1;
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
padding: 0 0.5rem 2rem 0;
}
.container {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
}
.collapseUp {
flex-direction: column-reverse;
}
.collapseContent {
will-change: transform, opacity;
transition: transform 0.3s cubic-bezier(0.4,0,0.2,1), opacity 0.3s cubic-bezier(0.4,0,0.2,1);
}
.collapseContent.collapsed {
transform: translateY(-100%);
opacity: 0;
}
.collapseContent.expanded {
transform: translateY(0);
opacity: 1;
}

View file

@ -1,100 +0,0 @@
import React, { useState, useEffect } from "react";
import { useSearchParams } from "react-router-dom";
import { MdExpandMore, MdExpandLess } from "react-icons/md";
import DashboardPromptSettings from './DashboardPromptSettings/DashboardPromptSettings';
import DashboardPromptSet from './DashboardPromptSet/DashboardPromptSet';
import { Prompt } from '../../../hooks/usePrompts';
import { useLanguage } from '../../../contexts/LanguageContext';
import styles from './DashboardPrompt.module.css';
interface DashboardPromptProps {
onPromptRun: (prompt: Prompt) => void;
isCollapsed: boolean;
onToggleCollapse: () => void;
}
const DashboardPrompt: React.FC<DashboardPromptProps> = ({
onPromptRun,
isCollapsed,
onToggleCollapse
}) => {
const { t } = useLanguage();
const [activeTab, setActiveTab] = useState(t('dashboard.prompt.template'));
const [searchParams] = useSearchParams();
useEffect(() => {
const expandedPrompt = searchParams.get('expandedPrompt');
const promptId = searchParams.get('promptId');
if (expandedPrompt) {
setActiveTab(t('dashboard.prompt.template'));
} else if (promptId) {
setActiveTab(t('dashboard.prompt.settings'));
}
}, [searchParams, t]);
return (
<div className={`${styles.dashboard_prompt} ${isCollapsed ? styles.collapsed : ''}`}>
<div className={ styles.prompt_header }>
<div className={ styles.prompt_button_div }>
{[
t('dashboard.prompt.template'),
t('dashboard.prompt.settings')
].map((tab) => (
<div key={tab} className={styles.buttonWrapper}>
<button
className={`${styles.prompt_button} ${
!isCollapsed
? (activeTab === tab ? styles.prompt_button_active : styles.prompt_button_inactive)
: styles.prompt_button_collapsed
}`}
onClick={()=> setActiveTab(tab)}
>
{ tab }
</button>
{!isCollapsed && activeTab === tab && (
<div className={styles.horizontalLine}></div>
)}
</div>
))}
</div>
<div
className={styles.expandIcon}
onClick={onToggleCollapse}
style={{
transform: !isCollapsed ? 'rotate(0deg)' : 'rotate(180deg)',
transition: 'transform 0.3s ease'
}}
>
<MdExpandLess size={24} />
</div>
</div>
{!isCollapsed && (
<div className={styles.horizontalLineLight}></div>
)}
{!isCollapsed && (
<div
className={styles.content_wrapper}
style={{
display: "flex",
flexDirection: "column",
flex: 1,
minHeight: 0
}}
>
<div style={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column' }}>
{activeTab === t('dashboard.prompt.template') ? (
<DashboardPromptSet onPromptRun={onPromptRun} />
) : (
<DashboardPromptSettings />
)}
</div>
</div>
)}
</div>
)
}
export default DashboardPrompt;

View file

@ -1,127 +0,0 @@
.container {
display: flex;
flex-direction: column;
overflow: hidden;
font-family: var(--font-family);
}
.header {
display: flex;
justify-content: left;
align-items: center;
margin-top: 1rem;
margin-bottom: 1rem;
flex-shrink: 0;
gap: 20px;
}
.addButton {
border-radius: 30px;
background: var(--color-secondary);
color: var(--color-bg);
border: none;
outline: none;
text-align: left;
padding-left: 20px;
padding-right: 20px;
padding-top: 10px;
padding-bottom: 10px;
display: flex;
gap: 10px;
align-items: center;
font-family: var(--font-family);
transition: background-color 0.2s ease;
}
.addButton:hover {
cursor: pointer;
background: var(--color-secondary-hover);
}
.promptCount {
font-size: 0.875rem;
color: var(--color-gray);
font-family: var(--font-family);
}
.scrollableContent {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
padding-right: 0.5rem;
padding-bottom: 2rem;
min-height: 0;
}
.scrollableContent::-webkit-scrollbar {
width: 6px;
}
.scrollableContent::-webkit-scrollbar-track {
background: transparent;
}
.scrollableContent::-webkit-scrollbar-thumb {
background: var(--color-gray-disabled);
border-radius: 3px;
}
.scrollableContent::-webkit-scrollbar-thumb:hover {
background: var(--color-gray);
}
.promptsList {
display: flex;
flex-direction: column;
gap: 14px;
padding-bottom: 1rem;
}
.loadingContainer {
display: flex;
justify-content: center;
align-items: center;
padding: 2rem;
}
.loadingText {
color: var(--color-gray);
font-family: var(--font-family);
}
.errorContainer {
padding: 1rem;
background-color: var(--color-red-disabled);
border: 1px solid var(--color-red);
border-radius: 0.5rem;
}
.errorText {
color: var(--color-red);
font-family: var(--font-family);
}
.retryButton {
margin-top: 0.5rem;
padding: 0.5rem 1rem;
background-color: var(--color-red);
color: var(--color-bg);
border-radius: 0.375rem;
border: none;
cursor: pointer;
transition: background-color 0.2s;
font-family: var(--font-family);
}
.retryButton:hover {
background-color: var(--color-red-hover);
}
.emptyState {
text-align: center;
padding: 2rem;
color: var(--color-gray);
font-family: var(--font-family);
}

View file

@ -1,96 +0,0 @@
import React, { useState } from 'react';
import { usePrompts, usePromptOperations, Prompt } from '../../../../hooks/usePrompts';
import { useLanguage } from '../../../../contexts/LanguageContext';
import DashboardPromptSetItem from './DashboardPromptSetItem';
import DashboardPromptSetModal from './DashboardPromptSetModal';
import styles from './DashboardPromptSet.module.css';
import { FaPlus } from 'react-icons/fa';
interface DashboardPromptSetProps {
onPromptRun: (prompt: Prompt) => void;
}
function DashboardPromptSet({ onPromptRun }: DashboardPromptSetProps) {
const { t } = useLanguage();
const { prompts, loading, error, refetch } = usePrompts();
const { handlePromptCreate, creatingPrompt } = usePromptOperations();
const [isModalOpen, setIsModalOpen] = useState(false);
const handleCreatePrompt = async (promptData: { name: string; content: string }) => {
const result = await handlePromptCreate(promptData);
if (result.success) {
await refetch(); // Refresh the prompts list
setIsModalOpen(false);
} else {
throw new Error(result.error);
}
};
if (loading) {
return (
<div className={styles.loadingContainer}>
<div className={styles.loadingText}>{t('promptset.loading')}</div>
</div>
);
}
if (error) {
return (
<div className={styles.errorContainer}>
<div className={styles.errorText}>{t('promptset.error.loading')}: {error}</div>
<button
onClick={refetch}
className={styles.retryButton}
>
{t('promptset.retry')}
</button>
</div>
);
}
return (
<div className={styles.container}>
<div className={styles.header}>
<div className={styles.headerButtons}>
<button className={styles.addButton} onClick={() => setIsModalOpen(true)}>
<FaPlus />
{t('promptset.new_prompt')}
</button>
</div>
<div className={styles.promptCount}>
{prompts.length} {prompts.length === 1 ? t('promptset.prompt_count') : t('promptset.prompt_count_plural')}
</div>
</div>
<div className={styles.scrollableContent}>
{prompts.length === 0 ? (
<div className={styles.emptyState}>
{t('promptset.no_prompts')}
</div>
) : (
<div className={styles.promptsList}>
{prompts.map((prompt) => (
<DashboardPromptSetItem
key={prompt.id}
prompt={prompt}
onDelete={refetch}
onRun={onPromptRun}
onShare={refetch}
/>
))}
</div>
)}
</div>
<DashboardPromptSetModal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
onSubmit={handleCreatePrompt}
isLoading={creatingPrompt}
/>
</div>
);
}
export default DashboardPromptSet;

View file

@ -1,182 +0,0 @@
.promptItem {
background: var(--color-surface);
border-radius: 30px;
display: flex;
padding: 20px;
flex-direction: column;
align-items: flex-start;
align-self: stretch;
justify-content: top;
font-family: var(--font-family);
gap: 11px;
font-size: 14px;
}
.promptMain {
display: flex;
justify-content: space-between;
align-items: flex-start;
align-self: stretch;
flex: 1;
min-height: 0;
}
.promptInfo {
flex: 1;
}
.promptName {
font-weight: 400;
color: var(--color-text);
margin:0;
font-family: var(--font-family);
}
.promptDate {
font: 14px;
color: var(--color-gray);
font-family: var(--font-family);
}
.promptText {
overflow: hidden;
height: auto;
flex: 1;
min-height: 0;
opacity: 0.5;
margin:0;
color: var(--color-text);
font-family: var(--font-family);
}
.promptText.p {
margin:0;
}
.actionButtons {
display: flex;
gap: 0.5rem;
align-self: flex-start;
flex-shrink: 0;
}
.actionButton {
padding: 0.5rem;
border-radius: 12px;
background: var(--color-secondary);
color: var(--color-bg);
cursor: pointer;
border: none;
font-family: var(--font-family);
transition: background-color 0.2s ease;
}
.actionButton:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.runButton {
border-radius: 12px;
background: var(--color-secondary);
color: var(--color-bg);
}
.runButton:hover:not(:disabled) {
border-radius: 12px;
background: var(--color-secondary-hover);
color: var(--color-bg);
cursor: pointer;
}
.shareButton {
border-radius: 12px;
background: var(--color-secondary);
color: var(--color-bg);
}
.shareButton:hover:not(:disabled) {
border-radius: 12px;
background: var(--color-secondary-hover);
color: var(--color-bg);
}
.deleteButton {
border-radius: 12px;
background: var(--color-red);
color: var(--color-bg);
}
.deleteButton:hover:not(:disabled) {
border-radius: 12px;
background: var(--color-red-hover);
color: var(--color-bg);
}
.deleteButton.confirm {
background-color: var(--color-red-disabled);
color: var(--color-red);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.deleteButton.confirm:hover:not(:disabled) {
background-color: var(--color-red-hover);
color: var(--color-bg);
}
.actionText {
font-size: 12px;
color: var(--color-gray);
animation: pulse 1.5s infinite;
white-space: nowrap;
font-family: var(--font-family);
margin-left: 4px;
}
.deleteButton.confirm .actionText {
color: var(--color-red);
animation: none;
}
@keyframes pulse {
0% {
opacity: 0.6;
}
50% {
opacity: 1;
}
100% {
opacity: 0.6;
}
}
.promptContent {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
gap: 11px;
max-width: calc(100% - 120px);
}
.errorMessage {
margin-top: 0.75rem;
padding: 0.5rem;
background-color: var(--color-red-disabled);
border: 1px solid var(--color-red);
border-radius: 0.25rem;
font-size: 0.875rem;
color: var(--color-red);
font-family: var(--font-family);
}
.deletingMessage {
margin-top: 0.75rem;
font-size: 0.875rem;
color: var(--color-gray);
font-family: var(--font-family);
}

View file

@ -1,155 +0,0 @@
import React, { useState, useRef, useEffect } from 'react';
import { FaArrowRight } from 'react-icons/fa';
import { AiOutlineDelete } from 'react-icons/ai';
import { BsShareFill } from 'react-icons/bs';
import { usePromptOperations, Prompt } from '../../../../hooks/usePrompts';
import { useLanguage } from '../../../../contexts/LanguageContext';
import PromptShareModal from './PromptShareModal';
import styles from './DashboardPromptSetItem.module.css';
interface DashboardPromptSetItemProps {
prompt: Prompt;
onDelete?: () => void;
onRun: (prompt: Prompt) => void;
onShare?: () => void;
}
function DashboardPromptSetItem({ prompt, onDelete, onRun, onShare }: DashboardPromptSetItemProps) {
const { t } = useLanguage();
const { handlePromptDelete, handlePromptShare, deletingPrompts, sharingPrompts, deleteError, shareError } = usePromptOperations();
const contentRef = useRef<HTMLDivElement>(null);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [showShareModal, setShowShareModal] = useState(false);
const isDeleting = deletingPrompts.has(prompt.id);
const isSharing = sharingPrompts.has(prompt.id);
const handleDeleteClick = async () => {
if (showDeleteConfirm) {
const success = await handlePromptDelete(prompt.id);
if (success && onDelete) {
onDelete();
}
setShowDeleteConfirm(false);
} else {
setShowDeleteConfirm(true);
}
};
const handleCancelDelete = () => {
setShowDeleteConfirm(false);
};
const handleRun = () => {
onRun(prompt);
};
const handleShare = () => {
setShowShareModal(true);
};
const handleShareSubmit = async (shareData: { userIds: number[]; message?: string; title?: string }) => {
const result = await handlePromptShare(prompt.id, shareData);
if (result.success) {
setShowShareModal(false);
if (onShare) {
onShare(); // Trigger refresh of prompts list
}
}
// Error handling is done by the hook
};
const handleCloseShareModal = () => {
setShowShareModal(false);
};
return (
<>
<div
className={styles.promptItem}
>
<div className={styles.promptMain}>
<div className={styles.promptContent}>
<div className={styles.promptInfo}>
<h3 className={styles.promptName}>
{prompt.name}
</h3>
{prompt.createdAt && (
<p className={styles.promptDate}>
{t('promptset.created')}: {new Date(prompt.createdAt).toLocaleDateString('de-DE')}
</p>
)}
</div>
<div ref={contentRef}>
<p className={styles.promptText}>
{prompt.content}
</p>
</div>
</div>
<div className={styles.actionButtons}>
<button
onClick={handleRun}
className={`${styles.actionButton} ${styles.runButton}`}
title={t('promptset.run_tooltip')}
>
<FaArrowRight size={16} />
</button>
<button
onClick={handleShare}
disabled={isSharing}
className={`${styles.actionButton} ${styles.shareButton}`}
title={t('promptset.share_tooltip')}
>
<BsShareFill size={16} />
{isSharing && <span className={styles.actionText}>{t('share_modal.sharing')}</span>}
</button>
<button
onClick={handleDeleteClick}
disabled={isDeleting}
className={`${styles.actionButton} ${styles.deleteButton} ${showDeleteConfirm ? styles.confirm : ''}`}
title={showDeleteConfirm ? t('promptset.confirm_delete') : t('promptset.delete_tooltip')}
onBlur={handleCancelDelete}
>
<AiOutlineDelete size={16} />
{isDeleting && <span className={styles.actionText}>{t('promptset.deleting')}</span>}
{showDeleteConfirm && <span className={styles.actionText}>{t('promptset.confirm_click')}</span>}
</button>
</div>
</div>
{deleteError && (
<div className={styles.errorMessage}>
{t('promptset.delete_error')}: {deleteError}
</div>
)}
{shareError && (
<div className={styles.errorMessage}>
{t('share_modal.share_error')}: {shareError}
</div>
)}
{isDeleting && (
<div className={styles.deletingMessage}>
{t('promptset.deleting_message')}
</div>
)}
</div>
{/* Share Modal */}
<PromptShareModal
isOpen={showShareModal}
onClose={handleCloseShareModal}
onSubmit={handleShareSubmit}
promptName={prompt.name}
isLoading={isSharing}
/>
</>
);
}
export default DashboardPromptSetItem;

View file

@ -1,192 +0,0 @@
.overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
padding: 20px;
}
.modal {
background: var(--color-bg);
border-radius: 12px;
border: 1px solid var(--color-gray);
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
width: 100%;
max-width: 500px;
max-height: 90vh;
overflow: hidden;
display: flex;
flex-direction: column;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24px 24px 0 24px;
border-bottom: 1px solid var(--color-gray);
margin-bottom: 20px;
}
.title {
font-size: 20px;
font-weight: 600;
color: var(--color-text);
margin: 0;
}
.closeButton {
background: none;
border: none;
padding: 8px;
cursor: pointer;
color: var(--color-text);
border-radius: 6px;
transition: all 0.2s;
}
.closeButton:hover {
background-color: var(--color-gray-disabled);
color: var(--color-text);
}
.closeButton:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.form {
padding: 0 24px 24px 24px;
flex: 1;
overflow-y: auto;
}
.formGroup {
margin-bottom: 20px;
}
.label {
display: block;
font-size: 14px;
font-weight: 500;
color: var(--color-text);
margin-bottom: 6px;
}
.input {
width: 100%;
padding: 12px 16px;
border: 1px solid var(--color-gray);
border-radius: 8px;
font-size: 14px;
transition: border-color 0.2s;
box-sizing: border-box;
background-color: var(--color-bg);
color: var(--color-text);
}
.input:focus {
outline: none;
border-color: var(--color-secondary);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.input:disabled {
background-color: var(--color-gray-disabled);
opacity: 0.7;
}
.textarea {
width: 100%;
padding: 12px 16px;
border: 1px solid var(--color-gray);
border-radius: 8px;
font-size: 14px;
font-family: inherit;
resize: vertical;
min-height: 120px;
transition: border-color 0.2s;
box-sizing: border-box;
background-color: var(--color-bg);
color: var(--color-text);
}
.textarea:focus {
outline: none;
border-color: var(--color-secondary);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
background-color: var(--color-bg);
color: var(--color-text);
}
.textarea:disabled {
background-color: var(--color-gray-disabled);
opacity: 0.7;
}
.error {
background-color: var(--color-red-disabled);
border: 1px solid var(--color-red);
padding: 12px 16px;
border-radius: 8px;
font-size: 14px;
margin-bottom: 20px;
}
.buttons {
display: flex;
gap: 12px;
justify-content: flex-end;
margin-top: 24px;
padding-top: 20px;
border-top: 1px solid var(--color-gray);
}
.cancelButton {
padding: 10px 20px;
border: 1px solid var(--color-red);
background: var(--color-red);
color: var(--color-text);
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.cancelButton:hover {
background-color: var(--color-red-hover);
border-color: var(--color-red-hover);
}
.cancelButton:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.submitButton {
padding: 10px 20px;
background: var(--color-secondary);
color: white;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s;
}
.submitButton:hover {
background: var(--color-secondary-hover);
}
.submitButton:disabled {
background: var(--color-secondary-disabled);
cursor: not-allowed;
}

View file

@ -1,129 +0,0 @@
import React, { useState } from 'react';
import { FaTimes } from 'react-icons/fa';
import { useLanguage } from '../../../../contexts/LanguageContext';
import styles from './DashboardPromptSetModal.module.css';
interface DashboardPromptSetModalProps {
isOpen: boolean;
onClose: () => void;
onSubmit: (promptData: { name: string; content: string }) => Promise<void>;
isLoading?: boolean;
}
function DashboardPromptSetModal({ isOpen, onClose, onSubmit, isLoading = false }: DashboardPromptSetModalProps) {
const { t } = useLanguage();
const [name, setName] = useState('');
const [content, setContent] = useState('');
const [error, setError] = useState<string | null>(null);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!name.trim()) {
setError(t('modal.name_required'));
return;
}
if (!content.trim()) {
setError(t('modal.content_required'));
return;
}
setError(null);
try {
await onSubmit({ name: name.trim(), content: content.trim() });
// Reset form on success
setName('');
setContent('');
onClose();
} catch (err: any) {
setError(err.message || t('modal.create_error'));
}
};
const handleClose = () => {
setName('');
setContent('');
setError(null);
onClose();
};
if (!isOpen) return null;
return (
<div className={styles.overlay}>
<div className={styles.modal}>
<div className={styles.header}>
<h2 className={styles.title}>{t('modal.create_prompt')}</h2>
<button
onClick={handleClose}
className={styles.closeButton}
disabled={isLoading}
>
<FaTimes />
</button>
</div>
<form onSubmit={handleSubmit} className={styles.form}>
<div className={styles.formGroup}>
<label htmlFor="promptName" className={styles.label}>
{t('modal.name_label')} *
</label>
<input
id="promptName"
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
className={styles.input}
placeholder={t('modal.name_placeholder')}
disabled={isLoading}
maxLength={100}
/>
</div>
<div className={styles.formGroup}>
<label htmlFor="promptContent" className={styles.label}>
{t('modal.content_label')} *
</label>
<textarea
id="promptContent"
value={content}
onChange={(e) => setContent(e.target.value)}
className={styles.textarea}
placeholder={t('modal.content_placeholder')}
disabled={isLoading}
rows={8}
/>
</div>
{error && (
<div className={styles.error}>
{error}
</div>
)}
<div className={styles.buttons}>
<button
type="button"
onClick={handleClose}
className={styles.cancelButton}
disabled={isLoading}
>
{t('modal.cancel')}
</button>
<button
type="submit"
className={styles.submitButton}
disabled={isLoading}
>
{isLoading ? t('modal.creating') : t('modal.create')}
</button>
</div>
</form>
</div>
</div>
);
}
export default DashboardPromptSetModal;

View file

@ -1,315 +0,0 @@
.overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
padding: 20px;
}
.modal {
background: var(--color-bg);
border-radius: 12px;
border: 1px solid var(--color-gray);
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
width: 100%;
max-width: 600px;
max-height: 90vh;
overflow: hidden;
display: flex;
flex-direction: column;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24px 24px 0 24px;
border-bottom: 1px solid var(--color-gray);
margin-bottom: 20px;
}
.title {
font-size: 20px;
font-weight: 600;
color: var(--color-text);
margin: 0;
word-break: break-word;
}
.closeButton {
background: none;
border: none;
padding: 8px;
cursor: pointer;
color: var(--color-text);
border-radius: 6px;
transition: all 0.2s;
flex-shrink: 0;
}
.closeButton:hover {
background-color: var(--color-gray-disabled);
color: var(--color-text);
}
.closeButton:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.form {
padding: 0 24px 24px 24px;
flex: 1;
overflow-y: auto;
}
.formGroup {
margin-bottom: 24px;
}
.sectionHeader {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.label {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
font-weight: 500;
color: var(--color-text);
}
.labelIcon {
font-size: 12px;
color: var(--color-secondary);
}
.selectAllButton {
background: none;
border: 1px solid var(--color-secondary);
color: var(--color-secondary);
padding: 6px 12px;
border-radius: 6px;
font-size: 12px;
cursor: pointer;
transition: all 0.2s;
}
.selectAllButton:hover {
background-color: var(--color-secondary);
color: white;
}
.selectAllButton:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.usersList {
border: 1px solid var(--color-gray);
border-radius: 8px;
max-height: 200px;
overflow-y: auto;
background-color: var(--color-bg);
}
.userItem {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
cursor: pointer;
transition: background-color 0.2s;
border-bottom: 1px solid var(--color-gray);
}
.userItem:last-child {
border-bottom: none;
}
.userItem:hover {
background-color: var(--color-gray-disabled);
}
.userItem.selected {
background-color: rgba(59, 130, 246, 0.1);
border-color: var(--color-secondary);
}
.checkbox {
cursor: pointer;
width: 16px;
height: 16px;
}
.userIcon {
color: var(--color-secondary);
font-size: 14px;
flex-shrink: 0;
}
.userInfo {
flex: 1;
min-width: 0;
}
.userName {
font-weight: 500;
color: var(--color-text);
font-size: 14px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.userUsername {
font-size: 12px;
color: var(--color-text-disabled);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.loading {
padding: 20px;
text-align: center;
color: var(--color-text-disabled);
font-style: italic;
}
.noUsers {
padding: 20px;
text-align: center;
color: var(--color-text-disabled);
font-style: italic;
}
.selectedCount {
margin-top: 8px;
font-size: 12px;
color: var(--color-secondary);
font-weight: 500;
}
.input {
width: 100%;
padding: 12px 16px;
border: 1px solid var(--color-gray);
border-radius: 8px;
font-size: 14px;
transition: border-color 0.2s;
box-sizing: border-box;
background-color: var(--color-bg);
color: var(--color-text);
}
.input:focus {
outline: none;
border-color: var(--color-secondary);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.input:disabled {
background-color: var(--color-gray-disabled);
opacity: 0.7;
}
.textarea {
width: 100%;
padding: 12px 16px;
border: 1px solid var(--color-gray);
border-radius: 8px;
font-size: 14px;
font-family: inherit;
resize: vertical;
min-height: 80px;
transition: border-color 0.2s;
box-sizing: border-box;
background-color: var(--color-bg);
color: var(--color-text);
}
.textarea:focus {
outline: none;
border-color: var(--color-secondary);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
background-color: var(--color-bg);
color: var(--color-text);
}
.textarea:disabled {
background-color: var(--color-gray-disabled);
opacity: 0.7;
}
.error {
background-color: var(--color-red-disabled);
border: 1px solid var(--color-red);
padding: 12px 16px;
border-radius: 8px;
font-size: 14px;
margin-bottom: 20px;
color: var(--color-red);
}
.buttons {
display: flex;
gap: 12px;
justify-content: flex-end;
margin-top: 24px;
padding-top: 20px;
border-top: 1px solid var(--color-gray);
}
.cancelButton {
padding: 10px 20px;
border: 1px solid var(--color-red);
background: var(--color-red);
color: white;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.cancelButton:hover {
background-color: var(--color-red-hover);
border-color: var(--color-red-hover);
}
.cancelButton:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.submitButton {
padding: 10px 20px;
background: var(--color-secondary);
color: white;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.submitButton:hover:not(:disabled) {
background-color: var(--color-secondary-hover);
}
.submitButton:disabled {
opacity: 0.5;
cursor: not-allowed;
}

View file

@ -1,249 +0,0 @@
import React, { useState, useEffect } from 'react';
import { FaTimes, FaUser, FaUsers } from 'react-icons/fa';
import { useLanguage } from '../../../../contexts/LanguageContext';
import { useUsers, User } from '../../../../hooks/usePrompts';
import styles from './PromptShareModal.module.css';
interface PromptShareModalProps {
isOpen: boolean;
onClose: () => void;
onSubmit: (shareData: { userIds: number[]; message?: string; title?: string }) => Promise<void>;
promptName: string;
isLoading?: boolean;
}
function PromptShareModal({ isOpen, onClose, onSubmit, promptName, isLoading = false }: PromptShareModalProps) {
const { t } = useLanguage();
const { users, loading: usersLoading, error: usersError } = useUsers();
const [selectedUsers, setSelectedUsers] = useState<Set<number>>(new Set());
const [message, setMessage] = useState('');
const [title, setTitle] = useState('');
const [error, setError] = useState<string | null>(null);
// Reset form when modal opens/closes
useEffect(() => {
if (isOpen) {
setSelectedUsers(new Set());
setMessage('');
setTitle('');
setError(null);
}
}, [isOpen]);
const handleUserToggle = (userId: number) => {
console.log('Toggling user:', userId); // Debug log
setSelectedUsers(prev => {
const newSet = new Set(prev);
if (newSet.has(userId)) {
newSet.delete(userId);
console.log('Removed user:', userId); // Debug log
} else {
newSet.add(userId);
console.log('Added user:', userId); // Debug log
}
console.log('New selected users:', Array.from(newSet)); // Debug log
return newSet;
});
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (selectedUsers.size === 0) {
setError(t('share_modal.no_users_selected'));
return;
}
setError(null);
try {
await onSubmit({
userIds: Array.from(selectedUsers),
message: message.trim() || undefined,
title: title.trim() || undefined
});
onClose();
} catch (err: any) {
setError(err.message || t('share_modal.share_error'));
}
};
const handleClose = () => {
setSelectedUsers(new Set());
setMessage('');
setTitle('');
setError(null);
onClose();
};
const handleSelectAll = () => {
if (selectedUsers.size === users.length) {
setSelectedUsers(new Set());
} else {
setSelectedUsers(new Set(users.map(user => user.id)));
}
};
if (!isOpen) return null;
return (
<div className={styles.overlay}>
<div className={styles.modal}>
<div className={styles.header}>
<h2 className={styles.title}>
{t('share_modal.title')} "{promptName}"
</h2>
<button
onClick={handleClose}
className={styles.closeButton}
disabled={isLoading}
>
<FaTimes />
</button>
</div>
<form onSubmit={handleSubmit} className={styles.form}>
{/* Users Selection */}
<div className={styles.formGroup}>
<div className={styles.sectionHeader}>
<label className={styles.label}>
<FaUsers className={styles.labelIcon} />
{t('share_modal.select_users')} *
</label>
<button
type="button"
onClick={handleSelectAll}
className={styles.selectAllButton}
disabled={isLoading || usersLoading}
>
{selectedUsers.size === users.length
? t('share_modal.deselect_all')
: t('share_modal.select_all')
}
</button>
</div>
<div className={styles.usersList}>
{usersLoading && (
<div className={styles.loading}>
{t('share_modal.loading_users')}
</div>
)}
{usersError && (
<div className={styles.error}>
{t('share_modal.error_loading_users')}: {usersError}
</div>
)}
{users.map(user => (
<label
key={user.id}
className={`${styles.userItem} ${selectedUsers.has(user.id) ? styles.selected : ''}`}
htmlFor={`user-${user.id}`}
>
<input
id={`user-${user.id}`}
type="checkbox"
checked={selectedUsers.has(user.id)}
onChange={() => {
console.log('Checkbox clicked for user:', user.id); // Debug log
handleUserToggle(user.id);
}}
className={styles.checkbox}
disabled={isLoading}
/>
<FaUser className={styles.userIcon} />
<div className={styles.userInfo}>
<div className={styles.userName}>
{user.fullName || user.username}
</div>
{user.fullName && user.username && (
<div className={styles.userUsername}>
@{user.username}
</div>
)}
</div>
</label>
))}
{!usersLoading && !usersError && users.length === 0 && (
<div className={styles.noUsers}>
{t('share_modal.no_users_available')}
</div>
)}
</div>
{selectedUsers.size > 0 && (
<div className={styles.selectedCount}>
{selectedUsers.size === 1
? t('share_modal.one_user_selected')
: t('share_modal.multiple_users_selected').replace('{count}', selectedUsers.size.toString())
}
</div>
)}
</div>
{/* Custom Title */}
<div className={styles.formGroup}>
<label htmlFor="shareTitle" className={styles.label}>
{t('share_modal.custom_title')}
</label>
<input
id="shareTitle"
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
className={styles.input}
placeholder={t('share_modal.title_placeholder')}
disabled={isLoading}
maxLength={100}
/>
</div>
{/* Message */}
<div className={styles.formGroup}>
<label htmlFor="shareMessage" className={styles.label}>
{t('share_modal.message')}
</label>
<textarea
id="shareMessage"
value={message}
onChange={(e) => setMessage(e.target.value)}
className={styles.textarea}
placeholder={t('share_modal.message_placeholder')}
disabled={isLoading}
rows={4}
/>
</div>
{error && (
<div className={styles.error}>
{error}
</div>
)}
<div className={styles.buttons}>
<button
type="button"
onClick={handleClose}
className={styles.cancelButton}
disabled={isLoading}
>
{t('modal.cancel')}
</button>
<button
type="submit"
className={styles.submitButton}
disabled={isLoading || selectedUsers.size === 0}
>
{isLoading ? t('share_modal.sharing') : t('share_modal.share')}
</button>
</div>
</form>
</div>
</div>
);
}
export default PromptShareModal;

View file

@ -1,39 +0,0 @@
.promptArea {
display: flex;
flex-direction: column;
height: 100%;
position: relative;
font-family: var(--font-family);
}
.cancelContainer {
position: absolute;
top: 10px;
right: 10px;
z-index: 10;
}
.cancelButton {
display: flex;
align-items: center;
gap: 4px;
padding: 8px 16px;
background: var(--color-bg);
border: 1px solid var(--color-gray-disabled);
border-radius: 20px;
color: var(--color-gray);
font-size: 14px;
cursor: pointer;
transition: all 0.2s;
font-family: var(--font-family);
}
.cancelButton:hover {
background-color: var(--color-surface);
border-color: var(--color-gray);
color: var(--color-text);
}
.cancelIcon {
font-size: 16px;
}

View file

@ -1,18 +0,0 @@
import React from 'react';
import { useLanguage } from '../../../../contexts/LanguageContext';
import styles from './DashboardPromptSettings.module.css';
function DashboardPromptSettings() {
const { t } = useLanguage();
return (
<div className={styles.container}>
<div className={styles.content}>
<h1 className={styles.title}>{t('prompt_settings.title')}</h1>
<p>{t('prompt_settings.content_placeholder')}</p>
</div>
</div>
);
}
export default DashboardPromptSettings;

View file

@ -130,6 +130,13 @@
box-sizing: border-box;
}
.filterInput:focus {
outline: none;
border-color: var(--color-secondary);
opacity: 1;
box-shadow: 0 0 0 2px rgba(var(--color-secondary), 0.1);
}
.filterInput::placeholder {
color: transparent;
}
@ -168,12 +175,7 @@
opacity: 1;
}
.filterInput:focus {
outline: none;
border-color: var(--color-secondary);
opacity: 1;
box-shadow: 0 0 0 2px rgba(var(--color-secondary), 0.1);
}
@ -218,7 +220,7 @@
border: 1px solid var(--color-primary);
border-radius: 25px;
background: var(--color-bg);
max-height: 600px;
max-height: 90%;
}
.loading {
@ -401,7 +403,7 @@ tbody .actionsColumn {
align-items: center;
gap: 10px;
padding: 15px;
border-top: 1px solid var(--color-gray-disabled);
border-top: 1px solid var(--color-primary);
background: var(--color-bg);
border-radius: 0 0 8px 8px;
}

View file

@ -0,0 +1,30 @@
.pageManager {
height: 100%;
width: 100%;
position: relative;
}
.pageInstance {
height: 100%;
width: 100%;
position: absolute;
top: 0;
left: 0;
z-index: 1;
}
.hiddenPreserved {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
visibility: hidden;
pointer-events: none;
z-index: -1;
}
.pageContent {
height: 100%;
width: 100%;
}

View file

@ -0,0 +1,192 @@
import React, { useEffect, useState, Suspense } from 'react';
import { useLocation } from 'react-router-dom';
import { motion, AnimatePresence } from 'framer-motion';
import { pageConfigs } from './pageConfigs';
import styles from './PageManager.module.css';
interface PageInstance {
path: string;
component: React.ReactElement;
isActive: boolean;
shouldPreserve: boolean;
}
interface PageManagerProps {
loadingComponent: React.ComponentType;
errorComponent: React.ComponentType;
}
const PageManager: React.FC<PageManagerProps> = ({ loadingComponent: LoadingComponent, errorComponent: ErrorComponent }) => {
const location = useLocation();
const [pageInstances, setPageInstances] = useState<Map<string, PageInstance>>(new Map());
// Get current path
const getCurrentPath = () => {
const path = location.pathname === '/' ? '/dashboard' : location.pathname;
return path.startsWith('/') ? path.slice(1) : path;
};
const currentPath = getCurrentPath();
useEffect(() => {
const config = pageConfigs.find(c => c.path === currentPath);
if (!config || !config.moduleEnabled) {
return;
}
setPageInstances(prev => {
const newInstances = new Map(prev);
// Update active states
newInstances.forEach((instance) => {
instance.isActive = instance.path === currentPath;
});
// Create instance if it doesn't exist
if (!newInstances.has(currentPath)) {
const Component = config.component;
const shouldPreserve = config.preserveState || false;
const pageInstance: PageInstance = {
path: currentPath,
component: (
<div className={styles.pageContent}>
<Suspense fallback={<LoadingComponent />}>
<Component key={shouldPreserve ? `${currentPath}-preserved` : `${currentPath}-${Date.now()}`} />
</Suspense>
</div>
),
isActive: true,
shouldPreserve
};
newInstances.set(currentPath, pageInstance);
if (import.meta.env.DEV) {
console.log(`🔥 PageManager: Created ${shouldPreserve ? 'PRESERVED' : 'regular'} instance for ${currentPath}`, {
totalInstances: newInstances.size,
preservedInstances: Array.from(newInstances.values()).filter(i => i.shouldPreserve).length
});
}
} else {
if (import.meta.env.DEV) {
const instance = newInstances.get(currentPath);
console.log(`♻️ PageManager: Reusing ${instance?.shouldPreserve ? 'PRESERVED' : 'regular'} instance for ${currentPath}`, {
totalInstances: newInstances.size,
preservedInstances: Array.from(newInstances.values()).filter(i => i.shouldPreserve).length
});
}
}
return newInstances;
});
// Clean up non-preserved, inactive instances with delay for smooth transitions
const cleanupTimer = setTimeout(() => {
setPageInstances(currentInstances => {
const updatedInstances = new Map(currentInstances);
const instancesToDelete: string[] = [];
updatedInstances.forEach((instance, path) => {
if (!instance.isActive && !instance.shouldPreserve) {
instancesToDelete.push(path);
}
});
instancesToDelete.forEach(path => {
if (import.meta.env.DEV) {
console.log(`🗑️ PageManager: Cleaning up non-preserved instance for ${path}`);
}
updatedInstances.delete(path);
});
return updatedInstances;
});
}, 500); // Wait for transition to complete before cleanup
return () => clearTimeout(cleanupTimer);
}, [currentPath]);
const config = pageConfigs.find(c => c.path === currentPath);
if (!config || !config.moduleEnabled) {
return <ErrorComponent />;
}
// Animation variants for smooth transitions
const pageVariants = {
initial: {
opacity: 0,
scale: 1,
y: 0
},
in: {
opacity: 1,
scale: 1,
y: 0
},
out: {
opacity: 0,
scale: 1,
y: 0
}
};
const pageTransition = {
type: "tween" as const,
ease: "easeInOut" as const,
duration: 0.2
};
return (
<div className={styles.pageManager}>
{Array.from(pageInstances.values()).map((instance) => {
const isVisible = instance.isActive;
const shouldAnimate = !instance.shouldPreserve; // Only animate non-preserved pages
if (instance.shouldPreserve) {
// Preserved pages: Always mounted, just show/hide with animations
return (
<motion.div
key={instance.path}
className={styles.pageInstance}
initial={false} // Don't animate initial mount for preserved pages
animate={{
opacity: isVisible ? 1 : 0,
}}
transition={pageTransition}
style={{
pointerEvents: isVisible ? 'auto' : 'none',
zIndex: isVisible ? 1 : 0
}}
>
{instance.component}
</motion.div>
);
} else if (isVisible) {
// Non-preserved pages: Use AnimatePresence for full mount/unmount
return (
<AnimatePresence key={instance.path} mode="wait">
<motion.div
key={instance.path}
className={styles.pageInstance}
initial="initial"
animate="in"
exit="out"
variants={pageVariants}
transition={pageTransition}
>
{instance.component}
</motion.div>
</AnimatePresence>
);
}
return null;
})}
</div>
);
};
export default PageManager;

View file

@ -0,0 +1,2 @@
export { default as PageManager } from './PageManager';
export { default } from './PageManager';

View file

@ -0,0 +1,40 @@
import React from 'react';
import { IconType } from 'react-icons';
// Extended page configuration interface that includes sidebar properties
export interface PageConfig {
// Core page properties
path: string;
component: React.ComponentType<any>;
// Module management
moduleEnabled?: boolean;
// Performance & caching
persistent?: boolean;
preserveState?: boolean;
preload?: boolean;
// Sidebar properties
id: string;
name: string;
icon: IconType;
order?: number; // For sidebar ordering
showInSidebar?: boolean; // Whether to show in sidebar (default: true)
// Lifecycle hooks
onActivate?: () => void | Promise<void>;
onDeactivate?: () => void | Promise<void>;
onLoad?: () => void | Promise<void>;
onUnload?: () => void | Promise<void>;
}
// Sidebar item interface for compatibility
export interface SidebarItem {
id: string;
name: string;
link: string;
icon: IconType;
moduleEnabled: boolean;
order: number;
}

View file

@ -0,0 +1,211 @@
import { PageConfig } from './pageConfigInterface';
import { lazy } from 'react';
// Import icons for sidebar
import { MdOutlineWorkOutline } from 'react-icons/md';
import { LuWorkflow, LuTicket } from "react-icons/lu";
import { GoGear } from "react-icons/go";
import { FaPlug, FaRegFileAlt, FaShare } from "react-icons/fa";
// Lazy load components for better performance
const Dashboard = lazy(() => import('../../pages/Home/Dashboard'));
const Dateien = lazy(() => import('../../pages/Home/Dateien'));
const TeamBereich = lazy(() => import('../../pages/Home/TeamBereich'));
const Connections = lazy(() => import('../../pages/Home/Connections'));
const Workflows = lazy(() => import('../../pages/Home/Workflows'));
const Einstellungen = lazy(() => import('../../pages/Home/Einstellungen'));
const TestSharepoint = lazy(() => import('../../pages/Home/TestSharepoint'));
// Page configuration with caching and lifecycle settings
export const pageConfigs: PageConfig[] = [
{
path: 'dashboard',
component: Dashboard,
persistent: true, // Keep dashboard in memory
preserveState: true, // Preserve component state when navigating away
preload: true, // Preload dashboard as it's the default page
moduleEnabled: true,
// Sidebar properties
id: '1',
name: 'Dashboard',
icon: LuTicket,
order: 1,
showInSidebar: true,
onActivate: async () => {
if (import.meta.env.DEV) console.log('Dashboard activated - state preserved');
// You can add analytics tracking here
},
onDeactivate: async () => {
if (import.meta.env.DEV) console.log('Dashboard deactivated - keeping state');
}
},
{
path: 'dateien',
component: Dateien,
persistent: false,
preload: true,
moduleEnabled: true,
// Sidebar properties
id: '3',
name: 'Dateien',
icon: FaRegFileAlt,
order: 2,
showInSidebar: true,
onActivate: async () => {
if (import.meta.env.DEV) console.log('Dateien activated');
},
onLoad: async () => {
if (import.meta.env.DEV) console.log('Dateien loaded - can initialize file lists here');
},
onUnload: async () => {
if (import.meta.env.DEV) console.log('Dateien unloaded - cleanup file references');
}
},
{
path: 'team-bereich',
component: TeamBereich,
persistent: false,
preload: true,
moduleEnabled: true,
// Sidebar properties
id: '2',
name: 'Team Bereich',
icon: MdOutlineWorkOutline,
order: 5,
showInSidebar: true,
onActivate: async () => {
if (import.meta.env.DEV) console.log('Team Bereich activated');
}
},
{
path: 'connections',
component: Connections,
persistent: false,
preload: true,
moduleEnabled: true,
// Sidebar properties
id: '5',
name: 'Connections',
icon: FaPlug,
order: 4,
showInSidebar: true,
onActivate: async () => {
if (import.meta.env.DEV) console.log('Connections activated');
},
onLoad: async () => {
if (import.meta.env.DEV) console.log('Connections loaded - can fetch connection data here');
}
},
{
path: 'workflows',
component: Workflows,
persistent: false, // Always keep workflows in memory to preserve state
preload: true, // Preload workflows for better performance
moduleEnabled: true,
// Sidebar properties
id: '4',
name: 'Workflows',
icon: LuWorkflow,
order: 3,
showInSidebar: true,
onActivate: async () => {
if (import.meta.env.DEV) console.log('Workflows activated - preserving workflow state');
},
onLoad: async () => {
if (import.meta.env.DEV) console.log('Workflows loaded - initializing workflow engine');
// Initialize workflow engine or restore workflow state
},
// Note: No onUnload for workflows since it's persistent
onDeactivate: async () => {
if (import.meta.env.DEV) console.log('Workflows deactivated but keeping in memory');
// Save current workflow state but don't unload
}
},
{
path: 'einstellungen',
component: Einstellungen,
persistent: false,
preload: true,
moduleEnabled: true,
// Sidebar properties
id: '7',
name: 'Einstellungen',
icon: GoGear,
order: 6,
showInSidebar: true,
onActivate: async () => {
if (import.meta.env.DEV) console.log('Einstellungen activated');
}
},
{
path: 'testSharepoint',
component: TestSharepoint,
persistent: false,
preload: true,
moduleEnabled: false, // Disabled by default - can be enabled for testing
// Sidebar properties
id: '8',
name: 'Test Sharepoint',
icon: FaShare,
order: 7,
showInSidebar: true,
onActivate: async () => {
if (import.meta.env.DEV) console.log('Test Sharepoint activated');
},
onLoad: async () => {
if (import.meta.env.DEV) console.log('Test Sharepoint loaded - can initialize test environment');
},
onUnload: async () => {
if (import.meta.env.DEV) console.log('Test Sharepoint unloaded - cleanup test resources');
}
}
];
// Helper function to enable/disable modules dynamically
export const updateModuleStatus = (path: string, enabled: boolean): PageConfig[] => {
return pageConfigs.map(config =>
config.path === path
? { ...config, moduleEnabled: enabled }
: config
);
};
// Helper function to toggle persistence for a page
export const updatePagePersistence = (path: string, persistent: boolean): PageConfig[] => {
return pageConfigs.map(config =>
config.path === path
? { ...config, persistent }
: config
);
};
// Helper function to toggle state preservation for a page
export const updateStatePreservation = (path: string, preserveState: boolean): PageConfig[] => {
return pageConfigs.map(config =>
config.path === path
? { ...config, preserveState }
: config
);
};
// Get current configuration for a page
export const getPageConfig = (path: string): PageConfig | undefined => {
return pageConfigs.find(config => config.path === path);
};
// Get sidebar items from page configs
export const getSidebarItems = () => {
return pageConfigs
.filter(config => config.showInSidebar !== false)
.sort((a, b) => (a.order || 0) - (b.order || 0))
.map(config => ({
id: config.id,
name: config.name,
link: `/${config.path}`,
icon: config.icon,
moduleEnabled: config.moduleEnabled ?? true,
order: config.order || 0
}));
};
export default pageConfigs;

View file

@ -4,7 +4,6 @@
flex-direction: column;
align-self: top;
height: calc(100vh);
overflow: hidden;
font-family: var(--font-family);
}
@ -22,9 +21,8 @@
flex-direction: column;
align-self: top;
background: var(--color-bg);
box-shadow: 0px 2px 6px 0px rgba(194, 194, 194, 0.30);
gap: 20px;
min-height: calc(100vh - 50px); /* Ensure minimum height but allow expansion */
min-height: 100%;
}
/* Page headers with consistent spacing */
@ -99,17 +97,18 @@
width: calc(100% + 60px);
background-color: var(--color-primary);
height: 1px;
margin-left: -30px;
margin-left: -25px;
margin-bottom: 0;
flex-shrink: 0;
}
/* Content areas */
.contentArea {
min-height: 0;
min-height: 100%;
display: flex;
flex-direction: column;
gap: 10px;
flex: 1;
}
@ -124,14 +123,10 @@
@media (max-width: 768px) {
.pageContainer {
margin: 10px;
height: calc(100vh - 20px);
max-height: calc(100vh - 20px);
}
.pageCard,
.pageCardWithContentPadding {
padding: 20px;
height: calc(100vh - 0px);
max-height: calc(100vh - 0px);
}
.pageHeader {
flex-direction: column;

View file

@ -2,7 +2,7 @@ import React from 'react'
import styles from './SidebarStyles/Sidebar.module.css'
import SidebarItem from './SidebarItem';
import useSidebarData from '../../contexts/SidebarData';
import useSidebarFromPageConfigs from '../../hooks/useSidebar';
import SidebarUser from './SidebarUser';
import { useSidebarLogic } from './sidebarLogic';
import { SidebarProps } from './sidebarTypes';
@ -62,7 +62,7 @@ const Sidebar: React.FC<SidebarProps> = ({ data }) => {
}
const SidebarWithData: React.FC = () => {
return <Sidebar data={useSidebarData()} />;
return <Sidebar data={useSidebarFromPageConfigs()} />;
};
export default SidebarWithData;

View file

@ -16,42 +16,71 @@ const SidebarItem: React.FC<SidebarItemProps> = ({
}) => {
const Icon = item.icon as React.ComponentType<React.SVGProps<SVGSVGElement>>;
const hasSubItems = item.submenu && item.submenu.length > 0;
const isDisabled = item.moduleEnabled === false;
const toggleSubmenu = (e: React.MouseEvent) => {
if (isDisabled) {
e.preventDefault();
return;
}
if (hasSubItems) {
e.preventDefault();
onToggle();
}
};
const handleLinkClick = (e: React.MouseEvent) => {
if (isDisabled) {
e.preventDefault();
return;
}
};
return (
<div className={`${styles.menu} ${isMinimized ? styles.minimized : ''}`}>
<li className={`${isActive ? styles.active : ""}`}>
<div className={`${styles.menu} ${isMinimized ? styles.minimized : ''} ${isDisabled ? styles.disabled : ''}`}>
<li className={`${isActive ? styles.active : ""} ${isDisabled ? styles.disabledItem : ""}`}>
{/* Icon is always present */}
{Icon && <Icon className={styles.icon} />}
{Icon && <Icon className={`${styles.icon} ${isDisabled ? styles.disabledIcon : ''}`} />}
{/* Text content - always present but hidden when minimized */}
{hasSubItems ? (
<a href="#" onClick={toggleSubmenu} className={styles.menuLink}>
<span className={styles.menuText}>{item.name}</span>
<IoIosArrowDown className={`${styles.hassubmenu} ${isOpen ? styles.rotated : ''}`} />
<a
href="#"
onClick={toggleSubmenu}
className={`${styles.menuLink} ${isDisabled ? styles.disabledLink : ''}`}
aria-disabled={isDisabled}
title={isDisabled ? `${item.name} (Module disabled)` : item.name}
>
<span className={`${styles.menuText} ${isDisabled ? styles.disabledText : ''}`}>
{item.name}
</span>
<IoIosArrowDown className={`${styles.hassubmenu} ${isOpen ? styles.rotated : ''} ${isDisabled ? styles.disabledArrow : ''}`} />
</a>
) : (
<Link to={item.link || "#"} className={styles.menuLink}>
<span className={styles.menuText}>{item.name}</span>
<Link
to={isDisabled ? "#" : (item.link || "#")}
className={`${styles.menuLink} ${isDisabled ? styles.disabledLink : ''}`}
onClick={handleLinkClick}
aria-disabled={isDisabled}
title={isDisabled ? `${item.name} (Module disabled)` : item.name}
>
<span className={`${styles.menuText} ${isDisabled ? styles.disabledText : ''}`}>
{item.name}
</span>
</Link>
)}
{isMinimized && (
{isMinimized && !isDisabled && (
<Link
to={item.link || "#"}
className={styles.minimizedOverlay}
title={item.name}
onClick={handleLinkClick}
/>
)}
</li>
{hasSubItems && !isMinimized && <SidebarSubmenu item={item} isOpen={isOpen} />}
{hasSubItems && !isMinimized && !isDisabled && <SidebarSubmenu item={item} isOpen={isOpen} />}
</div>
);
};

View file

@ -126,4 +126,48 @@
color: var(--color-bg);
}
/* Disabled item styles */
.menu.disabled,
.menu li.disabledItem {
opacity: 0.4;
pointer-events: none;
}
.menu li.disabledItem:hover {
background: transparent;
color: var(--color-text);
cursor: not-allowed;
}
.disabledLink {
color: var(--color-text) !important;
opacity: 0.6 !important;
cursor: not-allowed !important;
pointer-events: none !important;
}
.disabledText {
color: var(--color-text) !important;
opacity: 0.6 !important;
}
.disabledIcon {
opacity: 0.6 !important;
color: var(--color-text) !important;
}
.disabledArrow {
opacity: 0.6 !important;
color: var(--color-text) !important;
}
/* Ensure disabled items don't show hover effects */
.menu li.disabledItem:hover .disabledLink,
.menu li.disabledItem:hover .disabledText,
.menu li.disabledItem:hover .disabledIcon,
.menu li.disabledItem:hover .disabledArrow {
color: var(--color-text) !important;
opacity: 0.6 !important;
}

View file

@ -7,6 +7,7 @@ export interface SidebarItemData {
link?: string;
icon?: React.ComponentType<React.SVGProps<SVGSVGElement>>;
submenu?: SidebarSubmenuItemData[];
moduleEnabled?: boolean; // New property for module state
}
// Submenu item interface

View file

@ -1,65 +0,0 @@
import { MdOutlineWorkOutline } from 'react-icons/md';
import { BsChatDots } from "react-icons/bs";
import { LuWorkflow, LuTicket } from "react-icons/lu";
import { RiTeamLine } from "react-icons/ri";
import { BiInfoSquare } from "react-icons/bi";
import { GoGear } from "react-icons/go";
import { FaPlug, FaRegFileAlt } from "react-icons/fa";
import { TbLogs } from "react-icons/tb";
import { FaShare } from "react-icons/fa";
import { useMemo } from 'react';
import { useLanguage } from './LanguageContext';
const useSidebarData = () => {
const { t } = useLanguage();
return useMemo(() => [
{
id: '1',
name: t('nav.dashboard'),
link: '/dashboard',
icon: LuTicket,
},
{
id: '3',
name: t('nav.files'),
link: '/dateien',
icon: FaRegFileAlt,
},
{
id: '4',
name: t('nav.workflows'),
link: '/workflows',
icon: LuWorkflow ,
},
{
id: '5',
name: t('nav.connections'),
link: '/connections',
icon: FaPlug ,
},
{
id: '2',
name: t('nav.team'),
link: '/team-bereich',
icon: MdOutlineWorkOutline,
},
{
id: '7',
name: t('nav.settings'),
link: '/einstellungen',
icon: GoGear,
},
{
id: '8',
name: t('nav.testSharepoint'),
link: '/testSharepoint',
icon: FaShare,
},
], [t]);
}
export default useSidebarData;

39
src/hooks/useSidebar.ts Normal file
View file

@ -0,0 +1,39 @@
import { useMemo } from 'react';
import { getSidebarItems } from '../components/PageManager/pageConfigs';
import { SidebarItem } from '../components/PageManager/pageConfigInterface';
import { useLanguage } from '../contexts/LanguageContext';
// Hook to get sidebar items from page configurations
export const useSidebarFromPageConfigs = (): SidebarItem[] => {
const { t } = useLanguage();
return useMemo(() => {
const sidebarItems = getSidebarItems();
// Map the items with translations
return sidebarItems.map(item => ({
...item,
// You can add translations here later if needed
// For now, we'll use the names from pageConfigs directly
name: getTranslatedName(item.name, t)
}));
}, [t]);
};
// Helper function to get translated names
// This maps the page config names to translation keys
const getTranslatedName = (name: string, t: (key: string) => string): string => {
const translationMap: Record<string, string> = {
'Dashboard': t('nav.dashboard'),
'Dateien': t('nav.files'),
'Workflows': t('nav.workflows'),
'Connections': t('nav.connections'),
'Team Bereich': t('nav.team'),
'Einstellungen': t('nav.settings'),
'Test Sharepoint': t('nav.testSharepoint')
};
return translationMap[name] || name;
};
export default useSidebarFromPageConfigs;

View file

@ -187,35 +187,89 @@ export function useWorkflowStatus(workflowId: string | null) {
return { status, loading, error, refetch: fetchStatus };
}
// Workflow messages hook
export function useWorkflowMessages(workflowId: string | null, messageId?: string) {
// Enhanced workflow messages hook with better typing
export function useWorkflowMessages(workflowId: string | null) {
const [messages, setMessages] = useState<WorkflowMessage[]>([]);
const [lastFetchTime, setLastFetchTime] = useState<number>(0);
const { request, isLoading: loading, error } = useApiRequest<null, WorkflowMessage[]>();
const fetchMessages = async () => {
if (!workflowId) {
// Clear messages when no workflow is selected
setMessages([]);
return;
return [];
}
try {
console.log(`📡 Fetching messages for workflow: ${workflowId}`);
const data = await request({
url: `/api/workflows/${workflowId}/messages`,
method: 'get'
});
console.log(`📨 Raw API response for messages:`, {
url: `/api/workflows/${workflowId}/messages`,
responseType: typeof data,
isArray: Array.isArray(data),
length: data?.length,
rawData: data,
firstMessage: data?.[0],
firstMessageKeys: data?.[0] ? Object.keys(data[0]) : []
});
// Only update if data has actually changed
const hasChanged = JSON.stringify(data) !== JSON.stringify(messages);
if (hasChanged) {
console.log(`📝 Messages updated: ${messages.length}${data?.length || 0}`);
setMessages(data);
setLastFetchTime(Date.now());
} else {
console.log(`📝 No changes in messages (${data?.length || 0} messages)`);
}
return data;
} catch (error) {
console.error('Failed to fetch workflow messages:', error);
return [];
}
};
useEffect(() => {
fetchMessages();
}, [workflowId]);
return { messages, loading, error, refetch: fetchMessages, lastFetchTime };
}
// Get single workflow hook
export function useWorkflow(workflowId: string | null) {
const [workflow, setWorkflow] = useState<Workflow | null>(null);
const { request, isLoading: loading, error } = useApiRequest<null, Workflow>();
const fetchWorkflow = async () => {
if (!workflowId) {
setWorkflow(null);
return null;
}
try {
const data = await request({
url: `/api/workflows/${workflowId}/messages`,
method: 'get',
params: messageId ? { messageId } : undefined
url: `/api/workflows/${workflowId}`,
method: 'get'
});
setMessages(data);
setWorkflow(data);
return data;
} catch (error) {
// Error is already handled by useApiRequest
console.error('Failed to fetch workflow:', error);
return null;
}
};
useEffect(() => {
fetchMessages();
}, [workflowId, messageId]);
return { messages, loading, error, refetch: fetchMessages };
useEffect(() => {
fetchWorkflow();
}, [workflowId]);
return { workflow, loading, error, refetch: fetchWorkflow };
}
// Workflow logs hook

View file

@ -168,6 +168,23 @@ export default {
'chat.loading_workflow_messages': 'Workflow-Nachrichten werden geladen...',
'chat.start_conversation': 'Beginne ein Gespräch, indem du eine Nachricht eingibst, eine Vorlage auswählst oder einen vorherigen Workflow fortsetzt …',
// Chat Input Area
'chat.input.continue_workflow': 'Gespräch fortsetzen...',
'chat.input.enter_message': 'Oder geben Sie Ihre Nachricht ein...',
'chat.input.continuing_workflow': 'Workflow wird fortgesetzt',
'chat.input.workflow': 'Workflow',
'chat.input.files_attached': 'Datei',
'chat.input.files_attached_plural': 'Dateien',
'chat.input.files_attached_label': 'angehängt',
'chat.input.error_prefix': 'Fehler:',
'chat.input.attach_files': 'Dateien anhängen',
'chat.input.sending': 'Wird gesendet...',
'chat.input.processing': 'Wird verarbeitet...',
'chat.input.continue': 'Fortsetzen',
'chat.input.send': 'Senden',
'chat.input.new_chat': 'Neuer Chat',
'chat.input.using_prompt': 'Verwende Vorlage:',
// File Preview
'file_preview.loading': 'Vorschau wird geladen...',
'file_preview.error': 'Fehler',
@ -286,11 +303,6 @@ export default {
'files.type.audio': 'Audio',
'files.type.file': 'Datei',
// File Sources
'files.source.uploaded': 'Hochgeladen',
'files.source.created': 'KI Erstellt',
'files.source.shared': 'Geteilt',
// File Actions
'files.action.download': 'Herunterladen',
'files.action.delete': 'Löschen',

View file

@ -169,6 +169,23 @@ export default {
'chat.loading_workflow_messages': 'Loading workflow messages...',
'chat.start_conversation': 'Start a conversation by entering a message, selecting a template, or continuing a previous workflow...',
// Chat Input Area
'chat.input.continue_workflow': 'Continue the conversation...',
'chat.input.enter_message': 'Or enter your message...',
'chat.input.continuing_workflow': 'Continuing workflow',
'chat.input.workflow': 'Workflow',
'chat.input.files_attached': 'file',
'chat.input.files_attached_plural': 'files',
'chat.input.files_attached_label': 'attached',
'chat.input.error_prefix': 'Error:',
'chat.input.attach_files': 'Attach Files',
'chat.input.sending': 'Sending...',
'chat.input.processing': 'Processing...',
'chat.input.continue': 'Continue',
'chat.input.send': 'Send',
'chat.input.new_chat': 'New Chat',
'chat.input.using_prompt': 'Using prompt:',
// File Preview
'file_preview.loading': 'Loading preview...',
'file_preview.error': 'Error',

View file

@ -168,6 +168,23 @@ export default {
'chat.loading_workflow_messages': 'Chargement des messages de workflow...',
'chat.start_conversation': 'Commencez une conversation en entrant un message, en sélectionnant un modèle ou en continuant un workflow précédent...',
// Chat Input Area
'chat.input.continue_workflow': 'Continuer la conversation...',
'chat.input.enter_message': 'Ou entrez votre message...',
'chat.input.continuing_workflow': 'Workflow en cours',
'chat.input.workflow': 'Workflow',
'chat.input.files_attached': 'fichier',
'chat.input.files_attached_plural': 'fichiers',
'chat.input.files_attached_label': 'attaché',
'chat.input.error_prefix': 'Erreur:',
'chat.input.attach_files': 'Joindre des fichiers',
'chat.input.sending': 'Envoi...',
'chat.input.processing': 'Traitement...',
'chat.input.continue': 'Continuer',
'chat.input.send': 'Envoyer',
'chat.input.new_chat': 'Nouveau Chat',
'chat.input.using_prompt': 'Utilisation du modèle:',
// File Preview
'file_preview.loading': 'Chargement de l\'aperçu...',
'file_preview.error': 'Erreur',

View file

@ -1,6 +1,6 @@
import React, { useState } from 'react';
import styles from './HomeStyles/Connections.module.css';
import sharedStyles from './HomeStyles/pages.module.css';
import sharedStyles from '../../components/PageManager/pages.module.css';
import { IoIosLink } from 'react-icons/io';
import {
ConnectionsTable,

View file

@ -1,62 +1,87 @@
import React, { useState, useCallback, useMemo } from 'react';
import { useState, useCallback } from 'react';
import { IoMdRefresh } from 'react-icons/io';
import { Prompt } from '../../hooks/usePrompts';
import { useLanguage } from '../../contexts/LanguageContext';
import styles from './HomeStyles/Dashboard.module.css'
import sharedStyles from '../../components/PageManager/pages.module.css';
import DashboardChat from '../../components/Dashboard/DashboardChat/DashboardChat';
function Dashboard () {
const { t } = useLanguage();
const [isChatExpanded, setIsChatExpanded] = useState(false);
const [selectedPrompt, setSelectedPrompt] = useState<Prompt | null>(null);
const [isPromptAreaCollapsed, setIsPromptAreaCollapsed] = useState(false);
const [currentWorkflowId, setCurrentWorkflowId] = useState<string | null>(null);
const [workflowCompleted, setWorkflowCompleted] = useState(false);
const handleChatToggleExpand = () => {
setIsChatExpanded(!isChatExpanded);
};
const handlePromptRun = (prompt: Prompt) => {
setSelectedPrompt(prompt);
setIsPromptAreaCollapsed(true);
};
const handleWorkflowIdChange = useCallback((workflowId: string | null) => {
setCurrentWorkflowId(prevId => {
// Reset completion status when workflow changes
if (workflowId !== prevId) {
setWorkflowCompleted(false);
}
return workflowId;
});
}, [setCurrentWorkflowId, setWorkflowCompleted]);
const handleWorkflowCompletedChange = useCallback((completed: boolean) => {
setWorkflowCompleted(completed);
setCurrentWorkflowId(workflowId);
}, []);
const handleWorkflowResume = useCallback((workflowId: string) => {
// Set the workflow ID to resume it
setCurrentWorkflowId(workflowId);
// Reset completion status when resuming
setWorkflowCompleted(false);
// Switch to Chat Area tab to show the resumed workflow
}, []);
// Determine CSS classes based on states
const handleResetWorkflow = useCallback(() => {
setCurrentWorkflowId(null);
setSelectedPrompt(null);
}, []);
// Format workflow ID for display
const displayWorkflowId = currentWorkflowId ?
`${currentWorkflowId.substring(0, 8)}...` :
t('dashboard.log.no_workflow');
return (
<div className={styles.dashboardContainer}>
<div className={`${styles.chatLogContainer} ${isChatExpanded ? styles.expanded : ''}`}>
<div className={styles.chatArea}>
<DashboardChat
isExpanded={isChatExpanded}
onToggleExpand={handleChatToggleExpand}
selectedPrompt={selectedPrompt}
onPromptUsed={() => setSelectedPrompt(null)}
onWorkflowIdChange={handleWorkflowIdChange}
onWorkflowCompletedChange={handleWorkflowCompletedChange}
onWorkflowResume={handleWorkflowResume}
/>
<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' }}>
{currentWorkflowId && (
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', padding: '8px 16px', backgroundColor: 'var(--color-gray-disabled)', borderRadius: '20px' }}>
<span style={{ fontSize: '14px', color: 'var(--color-text)', fontWeight: '500' }}>
{t('dashboard.log.workflow')}: {displayWorkflowId}
</span>
</div>
)}
{currentWorkflowId && (
<button
className={sharedStyles.secondaryButton}
onClick={handleResetWorkflow}
aria-label="Reset workflow"
>
<span className={sharedStyles.buttonIcon}><IoMdRefresh /></span>
Reset
</button>
)}
</div>
</div>
<div className={sharedStyles.horizontalDivider}></div>
<div className={styles.dashboardContentArea}>
<div className={`${styles.chatLogContainer} ${isChatExpanded ? styles.expanded : ''}`}>
<div className={styles.chatArea}>
<DashboardChat
isExpanded={isChatExpanded}
onToggleExpand={handleChatToggleExpand}
selectedPrompt={selectedPrompt}
onPromptUsed={() => setSelectedPrompt(null)}
onWorkflowIdChange={handleWorkflowIdChange}
onWorkflowResume={handleWorkflowResume}
/>
</div>
</div>
</div>
</div>
</div>

View file

@ -2,7 +2,7 @@
import { useRef, useState } from 'react';
import { IoMdCloudUpload } from 'react-icons/io';
import { useLanguage } from '../../contexts/LanguageContext';
import sharedStyles from './HomeStyles/pages.module.css'
import sharedStyles from '../../components/PageManager/pages.module.css';
import styles from './HomeStyles/Dateien.module.css'
import { DateienTable } from '../../components/Dateien'
import { useFileOperations } from '../../hooks/useFiles';

View file

@ -1,6 +1,6 @@
import React, { useState, useEffect } from 'react';
import styles from './HomeStyles/Einstellungen.module.css';
import sharedStyles from './HomeStyles/pages.module.css';
import sharedStyles from '../../components/PageManager/pages.module.css';
import { useLanguage, Language } from '../../contexts/LanguageContext';
function Einstellungen() {

View file

@ -1,9 +1,10 @@
import { useEffect } from 'react';
import { Outlet, useLocation } from 'react-router-dom';
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import styles from './HomeStyles/Home.module.css'
import Sidebar from '../../components/Sidebar';
import PageManager from '../../components/PageManager';
import { AnimatePresence, motion } from "framer-motion";
@ -11,7 +12,23 @@ function Home () {
useEffect(()=> {
document.title = "PowerOn";
}, []);
const location = useLocation();
// Loading component
const LoadingComponent = () => (
<div className={styles.loadingContainer}>
Lade Seite...
</div>
);
// Error component
const ErrorComponent = () => (
<div className={styles.errorContainer}>
Seite nicht verfügbar oder deaktiviert
</div>
);
return (
<div className={styles.homeContainer}>
<div className={styles.body}>
@ -19,21 +36,14 @@ function Home () {
<Sidebar />
</div>
<div className={styles.homeContent}>
<AnimatePresence mode="wait">
<motion.div
key={location.pathname}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.3, ease: "easeInOut" }}
style={{ height: "100%", display: "flex", flexDirection: "column" }}
>
<Outlet />
</motion.div>
</AnimatePresence>
<PageManager
loadingComponent={LoadingComponent}
errorComponent={ErrorComponent}
/>
</div>
</div>
</div>)
</div>
);
}
export default Home;

View file

@ -1,11 +1,9 @@
.dashboardContainer {
.dashboardContentArea {
display: flex;
flex-direction: column;
gap: 20px;
font-family: var(--font-family);
width: 100%;
height: 100vh;
flex: 1;
min-height: 0;
overflow: hidden;
}
.chatLogContainer {
@ -13,6 +11,7 @@
flex-direction: column;
flex: 1;
min-height: 0;
overflow: hidden;
}
.chatArea {
@ -20,5 +19,22 @@
flex-direction: column;
flex: 1;
min-height: 0;
overflow: hidden;
}
/* Dashboard-specific styles */
.dashboardPageCard {
position: relative;
}
.verticalDivider {
position: absolute;
top: calc(86px + 20px + 1px); /* pageHeader height + gap + horizontalDivider height */
bottom: 0;
left: calc(66.666% - 9px);
width: 1px;
background-color: var(--color-primary);
z-index: 10;
pointer-events: none;
}

View file

@ -37,4 +37,29 @@
min-width: 0;
display: flex;
flex-direction: column;
}
/* Page Loader Styles */
.loadingContainer {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
color: var(--color-text);
font-family: var(--font-family);
font-size: 1rem;
opacity: 0.7;
}
.errorContainer {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
color: var(--color-text);
font-family: var(--font-family);
font-size: 1rem;
opacity: 0.5;
text-align: center;
padding: 20px;
}

View file

@ -1,5 +1,5 @@
import styles from './HomeStyles/TeamBereich.module.css'
import sharedStyles from './HomeStyles/pages.module.css'
import sharedStyles from '../../components/PageManager/pages.module.css';
import MitgliederItem from '../../components/Mitglieder/MitgliederItem';
import { IoPersonAddSharp } from "react-icons/io5";

View file

@ -1,7 +1,7 @@
import { useState } from 'react';
import { IoIosRefresh, IoIosLink } from 'react-icons/io';
import { useLanguage } from '../../contexts/LanguageContext';
import sharedStyles from './HomeStyles/pages.module.css'
import sharedStyles from '../../components/PageManager/pages.module.css';
import styles from './HomeStyles/TestSharepoint.module.css'
import { TestSharepointTable, useTestSharepointLogic } from '../../components/TestSharepoint'

View file

@ -1,5 +1,5 @@
import styles from './HomeStyles/Workflows.module.css'
import sharedStyles from './HomeStyles/pages.module.css'
import sharedStyles from '../../components/PageManager/pages.module.css';
import { WorkflowsTable } from '../../components/Workflows'
import { useLanguage } from '../../contexts/LanguageContext'