added page activator and cacher
This commit is contained in:
parent
c5ecd88e66
commit
22d582f72b
55 changed files with 2094 additions and 4048 deletions
17
src/App.tsx
17
src/App.tsx
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 && (
|
||||
|
|
|
|||
|
|
@ -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' }}>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 && (
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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 */
|
||||
}
|
||||
|
|
@ -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`
|
||||
194
src/components/Dashboard/DashboardChat/useWorkflowManager.ts
Normal file
194
src/components/Dashboard/DashboardChat/useWorkflowManager.ts
Normal 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];
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
@ -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;
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -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;
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
30
src/components/PageManager/PageManager.module.css
Normal file
30
src/components/PageManager/PageManager.module.css
Normal 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%;
|
||||
}
|
||||
192
src/components/PageManager/PageManager.tsx
Normal file
192
src/components/PageManager/PageManager.tsx
Normal 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;
|
||||
2
src/components/PageManager/index.ts
Normal file
2
src/components/PageManager/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export { default as PageManager } from './PageManager';
|
||||
export { default } from './PageManager';
|
||||
40
src/components/PageManager/pageConfigInterface.ts
Normal file
40
src/components/PageManager/pageConfigInterface.ts
Normal 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;
|
||||
}
|
||||
211
src/components/PageManager/pageConfigs.ts
Normal file
211
src/components/PageManager/pageConfigs.ts
Normal 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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
39
src/hooks/useSidebar.ts
Normal 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;
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue