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 { ProtectedRoute } from './auth/ProtectedRoute';
|
||||||
import { LanguageProvider } from './contexts/LanguageContext';
|
import { LanguageProvider } from './contexts/LanguageContext';
|
||||||
import Home from './pages/Home/Home';
|
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 the global light theme CSS variables as default
|
||||||
import './assets/styles/light.css';
|
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() {
|
function App() {
|
||||||
// Load saved theme preference on app mount
|
// Load saved theme preference on app mount
|
||||||
|
|
@ -49,14 +42,8 @@ function App() {
|
||||||
<Home />
|
<Home />
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
}>
|
}>
|
||||||
<Route index element={<Dashboard />} />
|
{/* All page routing is now handled by the Page Loader in Home.tsx */}
|
||||||
<Route path="dashboard" element={<Dashboard />} />
|
<Route path="*" element={null} />
|
||||||
<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 />} />
|
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
</Router>
|
</Router>
|
||||||
|
|
|
||||||
|
|
@ -24,13 +24,10 @@ const DashboardChat: React.FC<DashboardChatProps> = ({
|
||||||
onWorkflowResume
|
onWorkflowResume
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useLanguage();
|
const { t } = useLanguage();
|
||||||
const [activeTab, setActiveTab] = useState(t('dashboard.chat.area'));
|
|
||||||
const [resumeWorkflowId, setResumeWorkflowId] = useState<string | null>(null);
|
const [resumeWorkflowId, setResumeWorkflowId] = useState<string | null>(null);
|
||||||
|
|
||||||
const handleWorkflowResume = (workflowId: string) => {
|
const handleWorkflowResume = (workflowId: string) => {
|
||||||
// Switch to Chat Area tab first
|
// Switch to Chat Area tab first
|
||||||
setActiveTab(t('dashboard.chat.area'));
|
|
||||||
// Set the workflow ID to resume
|
|
||||||
setResumeWorkflowId(workflowId);
|
setResumeWorkflowId(workflowId);
|
||||||
// Then call the parent's resume handler
|
// Then call the parent's resume handler
|
||||||
if (onWorkflowResume) {
|
if (onWorkflowResume) {
|
||||||
|
|
|
||||||
|
|
@ -3,151 +3,105 @@ import MessageList from "./DashboardChatAreaMessageList";
|
||||||
import FilePreview from "./DashboardChatAreaFilePreview";
|
import FilePreview from "./DashboardChatAreaFilePreview";
|
||||||
import InputArea from "./DashboardChatAreaInput";
|
import InputArea from "./DashboardChatAreaInput";
|
||||||
import ConnectedFiles from "./DashboardChatAreaConnectedFiles";
|
import ConnectedFiles from "./DashboardChatAreaConnectedFiles";
|
||||||
import "./DashboardChatAreaStyles/grid.css";
|
|
||||||
import { DashboardChatAreaProps } from "./dashboardChatAreaTypes";
|
import { DashboardChatAreaProps } from "./dashboardChatAreaTypes";
|
||||||
|
import { useWorkflowManager } from "./useWorkflowManager";
|
||||||
|
import styles from './DashboardChatAreaStyles/DashboardChat.module.css';
|
||||||
|
|
||||||
const DashboardChatArea: React.FC<DashboardChatAreaProps> = ({
|
const DashboardChatArea: React.FC<DashboardChatAreaProps> = ({
|
||||||
selectedPrompt,
|
selectedPrompt,
|
||||||
onPromptUsed,
|
onPromptUsed,
|
||||||
onWorkflowIdChange,
|
onWorkflowIdChange,
|
||||||
onWorkflowCompletedChange,
|
onWorkflowCompletedChange,
|
||||||
resumeWorkflowId
|
resumeWorkflowId
|
||||||
}) => {
|
}) => {
|
||||||
// Grid sizing state
|
// Fixed grid layout - no resizing
|
||||||
const [horizontalSplit, setHorizontalSplit] = useState(60); // percentage
|
|
||||||
const [verticalSplit, setVerticalSplit] = useState(60); // percentage
|
|
||||||
const [isDragging, setIsDragging] = useState<'horizontal' | 'vertical' | null>(null);
|
|
||||||
|
|
||||||
// File selection state
|
// File selection state
|
||||||
const [selectedFile, setSelectedFile] = useState<any>(null);
|
const [selectedFile, setSelectedFile] = useState<any>(null);
|
||||||
const [attachedFiles, setAttachedFiles] = useState<any[]>([]);
|
const [attachedFiles, setAttachedFiles] = useState<any[]>([]);
|
||||||
|
|
||||||
// Workflow state
|
// Centralized workflow management
|
||||||
const [currentWorkflowId, setCurrentWorkflowId] = useState<string | null>(resumeWorkflowId || null);
|
const [workflowState, workflowActions] = useWorkflowManager(resumeWorkflowId);
|
||||||
|
|
||||||
// Update current workflow ID when resumeWorkflowId changes
|
// Notify parent when workflow ID changes
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (resumeWorkflowId !== currentWorkflowId) {
|
if (onWorkflowIdChange && workflowState.currentWorkflowId !== resumeWorkflowId) {
|
||||||
setCurrentWorkflowId(resumeWorkflowId);
|
onWorkflowIdChange(workflowState.currentWorkflowId);
|
||||||
}
|
}
|
||||||
}, [resumeWorkflowId, currentWorkflowId]);
|
}, [workflowState.currentWorkflowId, onWorkflowIdChange, resumeWorkflowId]);
|
||||||
|
|
||||||
// Handle workflow ID changes
|
// Notify parent when workflow is completed
|
||||||
const handleWorkflowIdChange = React.useCallback((workflowId: string | null) => {
|
React.useEffect(() => {
|
||||||
setCurrentWorkflowId(workflowId);
|
if (onWorkflowCompletedChange && workflowState.workflow) {
|
||||||
if (onWorkflowIdChange) {
|
const isCompleted = ['completed', 'failed', 'stopped'].includes(workflowState.workflow.status);
|
||||||
onWorkflowIdChange(workflowId);
|
onWorkflowCompletedChange(isCompleted);
|
||||||
}
|
}
|
||||||
}, [onWorkflowIdChange]);
|
}, [workflowState.workflow?.status, onWorkflowCompletedChange]);
|
||||||
|
|
||||||
// Handle resizing
|
// Auto-load workflow when resumeWorkflowId changes externally
|
||||||
const handleMouseDown = (direction: 'horizontal' | 'vertical') => (e: React.MouseEvent) => {
|
React.useEffect(() => {
|
||||||
e.preventDefault();
|
if (resumeWorkflowId && resumeWorkflowId !== workflowState.currentWorkflowId) {
|
||||||
setIsDragging(direction);
|
console.log(`🔄 Loading workflow from external prop: ${resumeWorkflowId}`);
|
||||||
};
|
workflowActions.loadWorkflow(resumeWorkflowId);
|
||||||
|
}
|
||||||
|
}, [resumeWorkflowId, workflowState.currentWorkflowId, workflowActions]);
|
||||||
|
|
||||||
const handleMouseMove = (e: MouseEvent) => {
|
// No resizing functionality needed
|
||||||
if (!isDragging) return;
|
|
||||||
|
|
||||||
const container = document.querySelector('.chat-grid') as HTMLElement;
|
console.log('🎯 DashboardChatArea render:', {
|
||||||
if (!container) return;
|
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') {
|
{/* Top Right: File Preview */}
|
||||||
const newSplit = ((e.clientY - rect.top) / rect.height) * 100;
|
<div className={`${styles.quadrant} ${styles.file_preview_quadrant}`}>
|
||||||
setHorizontalSplit(Math.max(20, Math.min(80, newSplit)));
|
<FilePreview selectedFile={selectedFile} />
|
||||||
} else if (isDragging === 'vertical') {
|
</div>
|
||||||
const newSplit = ((e.clientX - rect.left) / rect.width) * 100;
|
|
||||||
setVerticalSplit(Math.max(20, Math.min(80, newSplit)));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMouseUp = () => {
|
{/* Bottom Left: Input Area */}
|
||||||
setIsDragging(null);
|
<div className={`${styles.quadrant} ${styles.input_quadrant}`}>
|
||||||
};
|
<InputArea
|
||||||
|
selectedPrompt={selectedPrompt}
|
||||||
|
onPromptUsed={onPromptUsed}
|
||||||
|
workflowState={workflowState}
|
||||||
|
workflowActions={workflowActions}
|
||||||
|
onAttachedFilesChange={setAttachedFiles}
|
||||||
|
attachedFiles={attachedFiles}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
// Event listeners
|
{/* Bottom Right: Connected Files */}
|
||||||
React.useEffect(() => {
|
<div className={`${styles.quadrant} ${styles.connected_files_quadrant}`}>
|
||||||
if (isDragging) {
|
<ConnectedFiles
|
||||||
document.addEventListener('mousemove', handleMouseMove);
|
onFileSelect={setSelectedFile}
|
||||||
document.addEventListener('mouseup', handleMouseUp);
|
selectedFile={selectedFile}
|
||||||
document.body.style.cursor = isDragging === 'horizontal' ? 'ns-resize' : 'ew-resize';
|
attachedFiles={attachedFiles}
|
||||||
document.body.style.userSelect = 'none';
|
onRemoveFile={(fileId) => {
|
||||||
|
// If the removed file is currently selected, clear the selection
|
||||||
return () => {
|
if (selectedFile?.id === fileId) {
|
||||||
document.removeEventListener('mousemove', handleMouseMove);
|
setSelectedFile(null);
|
||||||
document.removeEventListener('mouseup', handleMouseUp);
|
}
|
||||||
document.body.style.cursor = '';
|
// Remove the file from attached files
|
||||||
document.body.style.userSelect = '';
|
setAttachedFiles(files => files.filter(f => f.id !== fileId));
|
||||||
};
|
}}
|
||||||
}
|
/>
|
||||||
}, [isDragging]);
|
</div>
|
||||||
|
</div>
|
||||||
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>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DashboardChatArea;
|
export default DashboardChatArea;
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,6 @@ const ConnectedFiles: React.FC<ConnectedFilesProps> = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ padding: '16px', height: '100%', overflow: 'auto' }}>
|
<div style={{ padding: '16px', height: '100%', overflow: 'auto' }}>
|
||||||
<h3>Connected Files</h3>
|
|
||||||
|
|
||||||
{/* Show attached files count */}
|
{/* Show attached files count */}
|
||||||
{attachedFiles.length > 0 && (
|
{attachedFiles.length > 0 && (
|
||||||
|
|
|
||||||
|
|
@ -91,7 +91,7 @@ const FilePreview: React.FC<FilePreviewProps> = ({ selectedFile }) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ padding: '16px', height: '100%', overflow: 'auto' }}>
|
<div style={{ padding: '16px', height: '100%', overflow: 'auto' }}>
|
||||||
<h3>File Preview</h3>
|
|
||||||
|
|
||||||
{!selectedFile && (
|
{!selectedFile && (
|
||||||
<p style={{ color: 'var(--color-gray)', textAlign: 'center' }}>
|
<p style={{ color: 'var(--color-gray)', textAlign: 'center' }}>
|
||||||
|
|
|
||||||
|
|
@ -1,187 +1,243 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import { useWorkflowOperations } from '../../../hooks/useWorkflows';
|
|
||||||
import { Prompt } from '../../../hooks/usePrompts';
|
import { Prompt } from '../../../hooks/usePrompts';
|
||||||
import FileAttachmentPopup from './FileAttachmentPopup';
|
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 {
|
interface InputAreaProps {
|
||||||
selectedPrompt?: Prompt | null;
|
selectedPrompt?: Prompt | null;
|
||||||
onPromptUsed?: () => void;
|
onPromptUsed?: () => void;
|
||||||
onWorkflowIdChange?: (workflowId: string | null) => void;
|
workflowState: WorkflowManagerState;
|
||||||
onAttachedFilesChange?: (files: AttachedFile[]) => void;
|
workflowActions: WorkflowManagerActions;
|
||||||
attachedFiles?: AttachedFile[];
|
onAttachedFilesChange?: (files: AttachedFile[]) => void;
|
||||||
|
attachedFiles?: AttachedFile[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AttachedFile {
|
interface AttachedFile {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
size: number;
|
size: number;
|
||||||
type: string;
|
type: string;
|
||||||
fileData?: File;
|
fileData?: File;
|
||||||
objectUrl?: string;
|
objectUrl?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const InputArea: React.FC<InputAreaProps> = ({
|
const InputArea: React.FC<InputAreaProps> = ({
|
||||||
selectedPrompt,
|
selectedPrompt,
|
||||||
onPromptUsed,
|
onPromptUsed,
|
||||||
onWorkflowIdChange,
|
workflowState,
|
||||||
onAttachedFilesChange,
|
workflowActions,
|
||||||
attachedFiles: externalAttachedFiles = []
|
onAttachedFilesChange,
|
||||||
|
attachedFiles: externalAttachedFiles = []
|
||||||
}) => {
|
}) => {
|
||||||
const [inputValue, setInputValue] = useState('');
|
const { t } = useLanguage();
|
||||||
const [showFilePopup, setShowFilePopup] = useState(false);
|
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
|
// Always use external attached files from parent component
|
||||||
const currentAttachedFiles = externalAttachedFiles;
|
const currentAttachedFiles = externalAttachedFiles;
|
||||||
const { startWorkflow, startingWorkflow, startError } = useWorkflowOperations();
|
|
||||||
|
|
||||||
// Auto-fill input when prompt is selected
|
// Auto-resize textarea function
|
||||||
useEffect(() => {
|
const adjustTextareaHeight = () => {
|
||||||
if (selectedPrompt) {
|
const textarea = textareaRef.current;
|
||||||
setInputValue(selectedPrompt.content);
|
if (!textarea) return;
|
||||||
}
|
|
||||||
}, [selectedPrompt]);
|
|
||||||
|
|
||||||
const handleSend = async () => {
|
// Reset height to auto to get the actual scroll height
|
||||||
if (!inputValue.trim() || startingWorkflow) return;
|
textarea.style.height = 'auto';
|
||||||
|
|
||||||
try {
|
// Calculate the height based on content
|
||||||
const result = await startWorkflow({
|
const scrollHeight = textarea.scrollHeight;
|
||||||
prompt: inputValue,
|
const lineHeight = 1.5 * 14; // 1.5em * 14px font size
|
||||||
listFileId: currentAttachedFiles.map(f => f.id)
|
const padding = 32; // 16px top + 16px bottom padding
|
||||||
});
|
const minHeight = lineHeight * 4 + padding; // 4 rows
|
||||||
|
const maxHeight = lineHeight * 8 + padding; // 8 rows
|
||||||
|
|
||||||
if (result.success) {
|
// Set height within constraints
|
||||||
setInputValue('');
|
const newHeight = Math.min(Math.max(scrollHeight, minHeight), maxHeight);
|
||||||
if (onAttachedFilesChange) {
|
textarea.style.height = `${newHeight}px`;
|
||||||
onAttachedFilesChange([]);
|
};
|
||||||
}
|
|
||||||
if (onPromptUsed) onPromptUsed();
|
|
||||||
if (onWorkflowIdChange && result.data?.id) {
|
|
||||||
onWorkflowIdChange(result.data.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to start workflow:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleKeyPress = (e: React.KeyboardEvent) => {
|
// Auto-fill input when prompt is selected
|
||||||
if (e.key === 'Enter' && !e.shiftKey) {
|
useEffect(() => {
|
||||||
e.preventDefault();
|
if (selectedPrompt) {
|
||||||
handleSend();
|
setInputValue(selectedPrompt.content);
|
||||||
}
|
}
|
||||||
};
|
}, [selectedPrompt]);
|
||||||
|
|
||||||
const handleFilesAttached = (files: AttachedFile[]) => {
|
// Adjust height when input value changes
|
||||||
setShowFilePopup(false);
|
useEffect(() => {
|
||||||
|
adjustTextareaHeight();
|
||||||
|
}, [inputValue]);
|
||||||
|
|
||||||
|
// Initial resize on mount
|
||||||
|
useEffect(() => {
|
||||||
|
adjustTextareaHeight();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
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) {
|
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 => {
|
const handleKeyPress = (e: React.KeyboardEvent) => {
|
||||||
if (bytes < 1024) return bytes + ' B';
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
if (bytes < 1024 * 1024) return Math.round(bytes / 1024) + ' KB';
|
e.preventDefault();
|
||||||
return Math.round(bytes / (1024 * 1024)) + ' MB';
|
handleSend();
|
||||||
};
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
const handleFilesAttached = (files: AttachedFile[]) => {
|
||||||
<div style={{ padding: '16px', height: '100%', display: 'flex', flexDirection: 'column' }}>
|
setShowFilePopup(false);
|
||||||
<h3>Input</h3>
|
if (onAttachedFilesChange) {
|
||||||
|
onAttachedFilesChange(files);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
{startError && (
|
|
||||||
<div style={{
|
|
||||||
padding: '8px',
|
|
||||||
backgroundColor: '#ffe6e6',
|
|
||||||
color: '#d00',
|
|
||||||
borderRadius: '4px',
|
|
||||||
marginBottom: '12px'
|
|
||||||
}}>
|
|
||||||
Error: {startError}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 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 }}>
|
const isWorkflowActive = workflowState.workflow &&
|
||||||
<textarea
|
['running', 'processing', 'started'].includes(workflowState.workflow.status);
|
||||||
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' }}>
|
// Determine if label should be in focused/moved state
|
||||||
<button
|
const shouldLabelBeFocused = isFocused || inputValue.trim().length > 0;
|
||||||
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
|
// Get placeholder text
|
||||||
onClick={handleSend}
|
const placeholderText = workflowState.currentWorkflowId
|
||||||
disabled={!inputValue.trim() || startingWorkflow}
|
? t('chat.input.continue_workflow')
|
||||||
style={{
|
: t('chat.input.enter_message');
|
||||||
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 && (
|
return (
|
||||||
<span style={{ fontSize: '12px', color: 'var(--color-gray)' }}>
|
<div className={styles.input_area_container}>
|
||||||
Using prompt: {selectedPrompt.name}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* File Attachment Popup */}
|
{/* Error messages */}
|
||||||
{showFilePopup && (
|
{(sendError || workflowState.error) && (
|
||||||
<FileAttachmentPopup
|
<div className={styles.error_message}>
|
||||||
onClose={() => setShowFilePopup(false)}
|
{t('chat.input.error_prefix')} {sendError || workflowState.error}
|
||||||
onFilesSelected={handleFilesAttached}
|
|
||||||
currentAttachedFiles={currentAttachedFiles}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</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;
|
export default InputArea;
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,9 @@ const MessageItem: React.FC<MessageItemProps> = ({ message, index, onFilePreview
|
||||||
console.log(`🎭 MessageItem rendering:`, {
|
console.log(`🎭 MessageItem rendering:`, {
|
||||||
messageId: message.id,
|
messageId: message.id,
|
||||||
messageRole: message.role,
|
messageRole: message.role,
|
||||||
|
content: message.content?.substring(0, 50) + (message.content?.length > 50 ? '...' : ''),
|
||||||
|
contentLength: message.content?.length || 0,
|
||||||
|
hasContent: !!message.content,
|
||||||
hasDocuments: !!(message.documents),
|
hasDocuments: !!(message.documents),
|
||||||
documentsArray: message.documents,
|
documentsArray: message.documents,
|
||||||
documentsLength: message.documents?.length || 0,
|
documentsLength: message.documents?.length || 0,
|
||||||
|
|
@ -171,7 +174,14 @@ const MessageItem: React.FC<MessageItemProps> = ({ message, index, onFilePreview
|
||||||
lineHeight: '1.5',
|
lineHeight: '1.5',
|
||||||
whiteSpace: 'pre-wrap'
|
whiteSpace: 'pre-wrap'
|
||||||
}}>
|
}}>
|
||||||
{message.content}
|
{message.content || (
|
||||||
|
<span style={{
|
||||||
|
color: 'var(--color-gray)',
|
||||||
|
fontStyle: 'italic'
|
||||||
|
}}>
|
||||||
|
[No message content]
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{hasDocuments && (
|
{hasDocuments && (
|
||||||
|
|
|
||||||
|
|
@ -1,255 +1,348 @@
|
||||||
import React, { useEffect, useRef, useState, useCallback } from 'react';
|
import React from 'react';
|
||||||
import { useWorkflowStatus } from '../../../hooks/useWorkflows';
|
|
||||||
import { Prompt } from '../../../hooks/usePrompts';
|
|
||||||
import { useApiRequest } from '../../../hooks/useApi';
|
import { useApiRequest } from '../../../hooks/useApi';
|
||||||
import MessageItem from './DashboardChatAreaMessageItem';
|
import MessageItem from './DashboardChatAreaMessageItem';
|
||||||
import { Message, Document, WorkflowMessage } from './dashboardChatAreaTypes';
|
import { WorkflowMessage, Document } from './dashboardChatAreaTypes';
|
||||||
|
import { WorkflowManagerState } from './useWorkflowManager';
|
||||||
|
|
||||||
interface MessageListProps {
|
interface MessageListProps {
|
||||||
selectedPrompt?: Prompt | null;
|
workflowState: WorkflowManagerState;
|
||||||
onPromptUsed?: () => void;
|
onFilePreview?: (file: any) => void;
|
||||||
resumeWorkflowId?: string | null;
|
|
||||||
onFilePreview?: (file: any) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Custom hook to fetch and transform messages like the old code
|
// Helper function to transform WorkflowMessage to display Message
|
||||||
const useTransformedMessages = (workflowId: string | null) => {
|
const transformWorkflowMessage = async (workflowMessage: WorkflowMessage, request: any): Promise<any> => {
|
||||||
const [messages, setMessages] = useState<Message[]>([]);
|
let documents: Document[] = [];
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
const { request } = useApiRequest();
|
|
||||||
|
|
||||||
const fetchMessages = useCallback(async () => {
|
// Fetch file metadata if fileIds exist
|
||||||
if (!workflowId) {
|
if (workflowMessage.fileIds && workflowMessage.fileIds.length > 0) {
|
||||||
setMessages([]);
|
console.log(`📎 Processing ${workflowMessage.fileIds.length} files for message ${workflowMessage.id}`);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoading(true);
|
const documentPromises = workflowMessage.fileIds.map(async (fileId, fileIndex) => {
|
||||||
setError(null);
|
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'
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
const document: Document = {
|
||||||
console.log(`🔍 Fetching messages for workflow: ${workflowId}`);
|
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
|
||||||
|
};
|
||||||
|
|
||||||
// Fetch workflow messages
|
console.log(`✅ File ${fileId} metadata processed:`, document.name);
|
||||||
const workflowMessages: WorkflowMessage[] = await request({
|
return document;
|
||||||
url: `/api/workflows/${workflowId}/messages`,
|
} catch (error) {
|
||||||
method: 'get'
|
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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
console.log(`📨 Received ${workflowMessages.length} messages from API:`, workflowMessages);
|
documents = await Promise.all(documentPromises);
|
||||||
|
}
|
||||||
|
|
||||||
// Debug each message structure
|
// Try different possible field names for content
|
||||||
workflowMessages.forEach((msg, index) => {
|
const possibleContent = workflowMessage.content ||
|
||||||
console.log(`📄 Message ${index + 1}:`, {
|
(workflowMessage as any).message ||
|
||||||
id: msg.id,
|
(workflowMessage as any).text ||
|
||||||
role: msg.role,
|
(workflowMessage as any).body ||
|
||||||
content: msg.content?.substring(0, 50) + '...',
|
'';
|
||||||
fileIds: msg.fileIds,
|
|
||||||
hasFileIds: !!(msg.fileIds && msg.fileIds.length > 0),
|
|
||||||
fileIdsLength: msg.fileIds?.length || 0
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Transform each message
|
const transformedMessage = {
|
||||||
const transformedMessages = await Promise.all(
|
id: workflowMessage.id,
|
||||||
workflowMessages.map(async (workflowMessage: WorkflowMessage, msgIndex) => {
|
role: workflowMessage.role,
|
||||||
console.log(`🔄 Transforming message ${msgIndex + 1} (${workflowMessage.id})`);
|
agentName: workflowMessage.role === 'user' ? 'You' : 'Assistant',
|
||||||
let documents: Document[] = [];
|
content: possibleContent,
|
||||||
|
timestamp: workflowMessage.timestamp,
|
||||||
|
documents: documents
|
||||||
|
};
|
||||||
|
|
||||||
// Fetch file metadata if fileIds exist
|
console.log(`🔄 Transformation result for ${workflowMessage.id}:`, {
|
||||||
if (workflowMessage.fileIds && workflowMessage.fileIds.length > 0) {
|
originalMessage: workflowMessage,
|
||||||
console.log(`📎 Message ${workflowMessage.id} has ${workflowMessage.fileIds.length} fileIds:`, workflowMessage.fileIds);
|
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
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const documentPromises = workflowMessage.fileIds.map(async (fileId, fileIndex) => {
|
return transformedMessage;
|
||||||
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 };
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const MessageList: React.FC<MessageListProps> = ({
|
const MessageList: React.FC<MessageListProps> = ({
|
||||||
selectedPrompt,
|
workflowState,
|
||||||
onPromptUsed,
|
onFilePreview
|
||||||
resumeWorkflowId,
|
|
||||||
onFilePreview
|
|
||||||
}) => {
|
}) => {
|
||||||
const { messages, loading, error, refetch } = useTransformedMessages(resumeWorkflowId || null);
|
const { request } = useApiRequest();
|
||||||
const { status } = useWorkflowStatus(resumeWorkflowId || null);
|
const [transformedMessages, setTransformedMessages] = React.useState<any[]>([]);
|
||||||
const intervalRef = useRef<number | null>(null);
|
const [isTransforming, setIsTransforming] = React.useState(false);
|
||||||
const [isInitialLoad, setIsInitialLoad] = React.useState(true);
|
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
|
// Transform messages when workflow messages change
|
||||||
useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (resumeWorkflowId && status?.status &&
|
const transformMessages = async () => {
|
||||||
(status.status === 'running' || status.status === 'processing' || status.status === 'started')) {
|
if (!workflowState.messages || workflowState.messages.length === 0) {
|
||||||
intervalRef.current = window.setInterval(() => {
|
setTransformedMessages([]);
|
||||||
console.log('🔄 Auto-refreshing messages due to active workflow');
|
return;
|
||||||
refetch();
|
}
|
||||||
}, 3000);
|
|
||||||
} else {
|
|
||||||
// Stop polling when completed, failed, or no workflow
|
|
||||||
if (intervalRef.current) {
|
|
||||||
clearInterval(intervalRef.current);
|
|
||||||
intervalRef.current = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
setIsTransforming(true);
|
||||||
if (intervalRef.current) {
|
console.log(`🔄 Transforming ${workflowState.messages.length} workflow messages...`);
|
||||||
clearInterval(intervalRef.current);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [resumeWorkflowId, status?.status, refetch]);
|
|
||||||
|
|
||||||
// Initial load when workflow ID changes
|
try {
|
||||||
useEffect(() => {
|
const transformed = await Promise.all(
|
||||||
if (resumeWorkflowId) {
|
workflowState.messages.map(async (msg: WorkflowMessage, index: number) => {
|
||||||
console.log(`🚀 Starting initial load for workflow: ${resumeWorkflowId}`);
|
console.log(`🔄 Transforming message ${index + 1}/${workflowState.messages.length}: ${msg.id}`);
|
||||||
setIsInitialLoad(true);
|
console.log(`📝 RAW API Message (${msg.role}):`, {
|
||||||
refetch().finally(() => setIsInitialLoad(false));
|
id: msg.id,
|
||||||
} else {
|
rawMessage: msg,
|
||||||
setIsInitialLoad(false);
|
contentType: typeof msg.content,
|
||||||
}
|
contentValue: msg.content,
|
||||||
}, [resumeWorkflowId, refetch]);
|
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)
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return await transformWorkflowMessage(msg, request);
|
||||||
<div style={{ padding: '16px', height: '100%', overflow: 'auto' }}>
|
})
|
||||||
<h3>Messages</h3>
|
);
|
||||||
|
|
||||||
{error && <p style={{ color: 'red' }}>Error: {error}</p>}
|
console.log(`✅ Successfully transformed ${transformed.length} messages`);
|
||||||
|
setTransformedMessages(transformed);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error transforming messages:', error);
|
||||||
|
setTransformedMessages([]);
|
||||||
|
} finally {
|
||||||
|
setIsTransforming(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
{status && (
|
transformMessages();
|
||||||
<div style={{
|
}, [workflowState.messages, request]);
|
||||||
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>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
// Check if user is scrolled near the bottom
|
||||||
{messages.map((message, index) => {
|
const checkScrollPosition = React.useCallback(() => {
|
||||||
console.log(`🎨 Rendering message ${message.id} with ${message.documents?.length || 0} documents`);
|
const container = scrollContainerRef.current;
|
||||||
return (
|
if (!container) return;
|
||||||
<MessageItem
|
|
||||||
key={message.id}
|
|
||||||
message={message}
|
|
||||||
index={index}
|
|
||||||
onFilePreview={onFilePreview}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{loading && (
|
const { scrollTop, scrollHeight, clientHeight } = container;
|
||||||
<div style={{ textAlign: 'center', padding: '16px' }}>
|
const distanceFromBottom = scrollHeight - scrollTop - clientHeight;
|
||||||
<p style={{ color: 'var(--color-gray)' }}>Loading messages...</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{messages.length === 0 && !isInitialLoad && !loading && (
|
// Consider "near bottom" if within 100px of the bottom
|
||||||
<p style={{ color: 'var(--color-gray)', textAlign: 'center' }}>
|
const isNearBottom = distanceFromBottom < 100;
|
||||||
No messages yet. Start a workflow to see messages here.
|
setIsUserScrolledUp(!isNearBottom);
|
||||||
</p>
|
|
||||||
)}
|
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>
|
</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;
|
export default MessageList;
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,271 @@
|
||||||
.dashboard_chat {
|
.dashboard_chat {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 20px;
|
flex-direction: column;
|
||||||
flex-direction: column; /* Fixed: was 'space-between' which is invalid */
|
|
||||||
align-self: stretch;
|
align-self: stretch;
|
||||||
background: var(--color-bg);
|
background: var(--color-bg);
|
||||||
position: relative;
|
position: relative;
|
||||||
box-shadow: 0px 2px 6px 0px rgba(194, 194, 194, 0.10);
|
height: 100%;
|
||||||
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
height: 100%; /* Fill parent height */
|
|
||||||
flex: 1; /* Take all available space from parent */
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
font-family: var(--font-family);
|
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;
|
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 {
|
.filterInput::placeholder {
|
||||||
color: transparent;
|
color: transparent;
|
||||||
}
|
}
|
||||||
|
|
@ -168,12 +175,7 @@
|
||||||
opacity: 1;
|
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: 1px solid var(--color-primary);
|
||||||
border-radius: 25px;
|
border-radius: 25px;
|
||||||
background: var(--color-bg);
|
background: var(--color-bg);
|
||||||
max-height: 600px;
|
max-height: 90%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading {
|
.loading {
|
||||||
|
|
@ -401,7 +403,7 @@ tbody .actionsColumn {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
border-top: 1px solid var(--color-gray-disabled);
|
border-top: 1px solid var(--color-primary);
|
||||||
background: var(--color-bg);
|
background: var(--color-bg);
|
||||||
border-radius: 0 0 8px 8px;
|
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;
|
flex-direction: column;
|
||||||
align-self: top;
|
align-self: top;
|
||||||
height: calc(100vh);
|
height: calc(100vh);
|
||||||
overflow: hidden;
|
|
||||||
font-family: var(--font-family);
|
font-family: var(--font-family);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -22,9 +21,8 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-self: top;
|
align-self: top;
|
||||||
background: var(--color-bg);
|
background: var(--color-bg);
|
||||||
box-shadow: 0px 2px 6px 0px rgba(194, 194, 194, 0.30);
|
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
min-height: calc(100vh - 50px); /* Ensure minimum height but allow expansion */
|
min-height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Page headers with consistent spacing */
|
/* Page headers with consistent spacing */
|
||||||
|
|
@ -99,17 +97,18 @@
|
||||||
width: calc(100% + 60px);
|
width: calc(100% + 60px);
|
||||||
background-color: var(--color-primary);
|
background-color: var(--color-primary);
|
||||||
height: 1px;
|
height: 1px;
|
||||||
margin-left: -30px;
|
margin-left: -25px;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Content areas */
|
/* Content areas */
|
||||||
.contentArea {
|
.contentArea {
|
||||||
min-height: 0;
|
min-height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -124,14 +123,10 @@
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.pageContainer {
|
.pageContainer {
|
||||||
margin: 10px;
|
margin: 10px;
|
||||||
height: calc(100vh - 20px);
|
height: calc(100vh - 0px);
|
||||||
max-height: calc(100vh - 20px);
|
max-height: calc(100vh - 0px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.pageCard,
|
|
||||||
.pageCardWithContentPadding {
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pageHeader {
|
.pageHeader {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
@ -2,7 +2,7 @@ import React from 'react'
|
||||||
|
|
||||||
import styles from './SidebarStyles/Sidebar.module.css'
|
import styles from './SidebarStyles/Sidebar.module.css'
|
||||||
import SidebarItem from './SidebarItem';
|
import SidebarItem from './SidebarItem';
|
||||||
import useSidebarData from '../../contexts/SidebarData';
|
import useSidebarFromPageConfigs from '../../hooks/useSidebar';
|
||||||
import SidebarUser from './SidebarUser';
|
import SidebarUser from './SidebarUser';
|
||||||
import { useSidebarLogic } from './sidebarLogic';
|
import { useSidebarLogic } from './sidebarLogic';
|
||||||
import { SidebarProps } from './sidebarTypes';
|
import { SidebarProps } from './sidebarTypes';
|
||||||
|
|
@ -62,7 +62,7 @@ const Sidebar: React.FC<SidebarProps> = ({ data }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const SidebarWithData: React.FC = () => {
|
const SidebarWithData: React.FC = () => {
|
||||||
return <Sidebar data={useSidebarData()} />;
|
return <Sidebar data={useSidebarFromPageConfigs()} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SidebarWithData;
|
export default SidebarWithData;
|
||||||
|
|
@ -16,42 +16,71 @@ const SidebarItem: React.FC<SidebarItemProps> = ({
|
||||||
}) => {
|
}) => {
|
||||||
const Icon = item.icon as React.ComponentType<React.SVGProps<SVGSVGElement>>;
|
const Icon = item.icon as React.ComponentType<React.SVGProps<SVGSVGElement>>;
|
||||||
const hasSubItems = item.submenu && item.submenu.length > 0;
|
const hasSubItems = item.submenu && item.submenu.length > 0;
|
||||||
|
const isDisabled = item.moduleEnabled === false;
|
||||||
|
|
||||||
const toggleSubmenu = (e: React.MouseEvent) => {
|
const toggleSubmenu = (e: React.MouseEvent) => {
|
||||||
|
if (isDisabled) {
|
||||||
|
e.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (hasSubItems) {
|
if (hasSubItems) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
onToggle();
|
onToggle();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleLinkClick = (e: React.MouseEvent) => {
|
||||||
|
if (isDisabled) {
|
||||||
|
e.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`${styles.menu} ${isMinimized ? styles.minimized : ''}`}>
|
<div className={`${styles.menu} ${isMinimized ? styles.minimized : ''} ${isDisabled ? styles.disabled : ''}`}>
|
||||||
<li className={`${isActive ? styles.active : ""}`}>
|
<li className={`${isActive ? styles.active : ""} ${isDisabled ? styles.disabledItem : ""}`}>
|
||||||
{/* Icon is always present */}
|
{/* 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 */}
|
{/* Text content - always present but hidden when minimized */}
|
||||||
{hasSubItems ? (
|
{hasSubItems ? (
|
||||||
<a href="#" onClick={toggleSubmenu} className={styles.menuLink}>
|
<a
|
||||||
<span className={styles.menuText}>{item.name}</span>
|
href="#"
|
||||||
<IoIosArrowDown className={`${styles.hassubmenu} ${isOpen ? styles.rotated : ''}`} />
|
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>
|
</a>
|
||||||
) : (
|
) : (
|
||||||
<Link to={item.link || "#"} className={styles.menuLink}>
|
<Link
|
||||||
<span className={styles.menuText}>{item.name}</span>
|
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>
|
</Link>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
{isMinimized && (
|
{isMinimized && !isDisabled && (
|
||||||
<Link
|
<Link
|
||||||
to={item.link || "#"}
|
to={item.link || "#"}
|
||||||
className={styles.minimizedOverlay}
|
className={styles.minimizedOverlay}
|
||||||
title={item.name}
|
title={item.name}
|
||||||
|
onClick={handleLinkClick}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</li>
|
</li>
|
||||||
{hasSubItems && !isMinimized && <SidebarSubmenu item={item} isOpen={isOpen} />}
|
{hasSubItems && !isMinimized && !isDisabled && <SidebarSubmenu item={item} isOpen={isOpen} />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -126,4 +126,48 @@
|
||||||
color: var(--color-bg);
|
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;
|
link?: string;
|
||||||
icon?: React.ComponentType<React.SVGProps<SVGSVGElement>>;
|
icon?: React.ComponentType<React.SVGProps<SVGSVGElement>>;
|
||||||
submenu?: SidebarSubmenuItemData[];
|
submenu?: SidebarSubmenuItemData[];
|
||||||
|
moduleEnabled?: boolean; // New property for module state
|
||||||
}
|
}
|
||||||
|
|
||||||
// Submenu item interface
|
// 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 };
|
return { status, loading, error, refetch: fetchStatus };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Workflow messages hook
|
// Enhanced workflow messages hook with better typing
|
||||||
export function useWorkflowMessages(workflowId: string | null, messageId?: string) {
|
export function useWorkflowMessages(workflowId: string | null) {
|
||||||
const [messages, setMessages] = useState<WorkflowMessage[]>([]);
|
const [messages, setMessages] = useState<WorkflowMessage[]>([]);
|
||||||
|
const [lastFetchTime, setLastFetchTime] = useState<number>(0);
|
||||||
const { request, isLoading: loading, error } = useApiRequest<null, WorkflowMessage[]>();
|
const { request, isLoading: loading, error } = useApiRequest<null, WorkflowMessage[]>();
|
||||||
|
|
||||||
const fetchMessages = async () => {
|
const fetchMessages = async () => {
|
||||||
if (!workflowId) {
|
if (!workflowId) {
|
||||||
// Clear messages when no workflow is selected
|
|
||||||
setMessages([]);
|
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 {
|
try {
|
||||||
const data = await request({
|
const data = await request({
|
||||||
url: `/api/workflows/${workflowId}/messages`,
|
url: `/api/workflows/${workflowId}`,
|
||||||
method: 'get',
|
method: 'get'
|
||||||
params: messageId ? { messageId } : undefined
|
|
||||||
});
|
});
|
||||||
|
|
||||||
setMessages(data);
|
setWorkflow(data);
|
||||||
|
return data;
|
||||||
} catch (error) {
|
} 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
|
// Workflow logs hook
|
||||||
|
|
|
||||||
|
|
@ -168,6 +168,23 @@ export default {
|
||||||
'chat.loading_workflow_messages': 'Workflow-Nachrichten werden geladen...',
|
'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.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
|
||||||
'file_preview.loading': 'Vorschau wird geladen...',
|
'file_preview.loading': 'Vorschau wird geladen...',
|
||||||
'file_preview.error': 'Fehler',
|
'file_preview.error': 'Fehler',
|
||||||
|
|
@ -286,11 +303,6 @@ export default {
|
||||||
'files.type.audio': 'Audio',
|
'files.type.audio': 'Audio',
|
||||||
'files.type.file': 'Datei',
|
'files.type.file': 'Datei',
|
||||||
|
|
||||||
// File Sources
|
|
||||||
'files.source.uploaded': 'Hochgeladen',
|
|
||||||
'files.source.created': 'KI Erstellt',
|
|
||||||
'files.source.shared': 'Geteilt',
|
|
||||||
|
|
||||||
// File Actions
|
// File Actions
|
||||||
'files.action.download': 'Herunterladen',
|
'files.action.download': 'Herunterladen',
|
||||||
'files.action.delete': 'Löschen',
|
'files.action.delete': 'Löschen',
|
||||||
|
|
|
||||||
|
|
@ -169,6 +169,23 @@ export default {
|
||||||
'chat.loading_workflow_messages': 'Loading workflow messages...',
|
'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.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
|
||||||
'file_preview.loading': 'Loading preview...',
|
'file_preview.loading': 'Loading preview...',
|
||||||
'file_preview.error': 'Error',
|
'file_preview.error': 'Error',
|
||||||
|
|
|
||||||
|
|
@ -168,6 +168,23 @@ export default {
|
||||||
'chat.loading_workflow_messages': 'Chargement des messages de workflow...',
|
'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.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
|
||||||
'file_preview.loading': 'Chargement de l\'aperçu...',
|
'file_preview.loading': 'Chargement de l\'aperçu...',
|
||||||
'file_preview.error': 'Erreur',
|
'file_preview.error': 'Erreur',
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import styles from './HomeStyles/Connections.module.css';
|
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 { IoIosLink } from 'react-icons/io';
|
||||||
import {
|
import {
|
||||||
ConnectionsTable,
|
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 { Prompt } from '../../hooks/usePrompts';
|
||||||
|
import { useLanguage } from '../../contexts/LanguageContext';
|
||||||
import styles from './HomeStyles/Dashboard.module.css'
|
import styles from './HomeStyles/Dashboard.module.css'
|
||||||
|
import sharedStyles from '../../components/PageManager/pages.module.css';
|
||||||
|
|
||||||
import DashboardChat from '../../components/Dashboard/DashboardChat/DashboardChat';
|
import DashboardChat from '../../components/Dashboard/DashboardChat/DashboardChat';
|
||||||
|
|
||||||
function Dashboard () {
|
function Dashboard () {
|
||||||
|
const { t } = useLanguage();
|
||||||
const [isChatExpanded, setIsChatExpanded] = useState(false);
|
const [isChatExpanded, setIsChatExpanded] = useState(false);
|
||||||
const [selectedPrompt, setSelectedPrompt] = useState<Prompt | null>(null);
|
const [selectedPrompt, setSelectedPrompt] = useState<Prompt | null>(null);
|
||||||
const [isPromptAreaCollapsed, setIsPromptAreaCollapsed] = useState(false);
|
|
||||||
const [currentWorkflowId, setCurrentWorkflowId] = useState<string | null>(null);
|
const [currentWorkflowId, setCurrentWorkflowId] = useState<string | null>(null);
|
||||||
const [workflowCompleted, setWorkflowCompleted] = useState(false);
|
|
||||||
|
|
||||||
const handleChatToggleExpand = () => {
|
const handleChatToggleExpand = () => {
|
||||||
setIsChatExpanded(!isChatExpanded);
|
setIsChatExpanded(!isChatExpanded);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePromptRun = (prompt: Prompt) => {
|
|
||||||
setSelectedPrompt(prompt);
|
|
||||||
setIsPromptAreaCollapsed(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleWorkflowIdChange = useCallback((workflowId: string | null) => {
|
const handleWorkflowIdChange = useCallback((workflowId: string | null) => {
|
||||||
setCurrentWorkflowId(prevId => {
|
setCurrentWorkflowId(workflowId);
|
||||||
// Reset completion status when workflow changes
|
|
||||||
if (workflowId !== prevId) {
|
|
||||||
setWorkflowCompleted(false);
|
|
||||||
}
|
|
||||||
return workflowId;
|
|
||||||
});
|
|
||||||
}, [setCurrentWorkflowId, setWorkflowCompleted]);
|
|
||||||
|
|
||||||
const handleWorkflowCompletedChange = useCallback((completed: boolean) => {
|
|
||||||
setWorkflowCompleted(completed);
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleWorkflowResume = useCallback((workflowId: string) => {
|
const handleWorkflowResume = useCallback((workflowId: string) => {
|
||||||
// Set the workflow ID to resume it
|
// Set the workflow ID to resume it
|
||||||
setCurrentWorkflowId(workflowId);
|
setCurrentWorkflowId(workflowId);
|
||||||
// Reset completion status when resuming
|
|
||||||
setWorkflowCompleted(false);
|
|
||||||
// Switch to Chat Area tab to show the resumed workflow
|
// 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 (
|
return (
|
||||||
<div className={styles.dashboardContainer}>
|
<div className={sharedStyles.pageContainer}>
|
||||||
<div className={`${styles.chatLogContainer} ${isChatExpanded ? styles.expanded : ''}`}>
|
<div className={`${sharedStyles.pageCard} ${styles.dashboardPageCard}`}>
|
||||||
<div className={styles.chatArea}>
|
{/* Vertical Divider - spans from after title to bottom */}
|
||||||
<DashboardChat
|
<div className={styles.verticalDivider}></div>
|
||||||
isExpanded={isChatExpanded}
|
|
||||||
onToggleExpand={handleChatToggleExpand}
|
<div className={sharedStyles.pageHeader}>
|
||||||
selectedPrompt={selectedPrompt}
|
<h1 className={sharedStyles.pageTitle}>{t('nav.dashboard')}</h1>
|
||||||
onPromptUsed={() => setSelectedPrompt(null)}
|
<div style={{ display: 'flex', gap: '15px', alignItems: 'center' }}>
|
||||||
onWorkflowIdChange={handleWorkflowIdChange}
|
{currentWorkflowId && (
|
||||||
onWorkflowCompletedChange={handleWorkflowCompletedChange}
|
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', padding: '8px 16px', backgroundColor: 'var(--color-gray-disabled)', borderRadius: '20px' }}>
|
||||||
onWorkflowResume={handleWorkflowResume}
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
import { useRef, useState } from 'react';
|
import { useRef, useState } from 'react';
|
||||||
import { IoMdCloudUpload } from 'react-icons/io';
|
import { IoMdCloudUpload } from 'react-icons/io';
|
||||||
import { useLanguage } from '../../contexts/LanguageContext';
|
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 styles from './HomeStyles/Dateien.module.css'
|
||||||
import { DateienTable } from '../../components/Dateien'
|
import { DateienTable } from '../../components/Dateien'
|
||||||
import { useFileOperations } from '../../hooks/useFiles';
|
import { useFileOperations } from '../../hooks/useFiles';
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import styles from './HomeStyles/Einstellungen.module.css';
|
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';
|
import { useLanguage, Language } from '../../contexts/LanguageContext';
|
||||||
|
|
||||||
function Einstellungen() {
|
function Einstellungen() {
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { Outlet, useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
import styles from './HomeStyles/Home.module.css'
|
import styles from './HomeStyles/Home.module.css'
|
||||||
|
|
||||||
import Sidebar from '../../components/Sidebar';
|
import Sidebar from '../../components/Sidebar';
|
||||||
|
import PageManager from '../../components/PageManager';
|
||||||
|
|
||||||
import { AnimatePresence, motion } from "framer-motion";
|
import { AnimatePresence, motion } from "framer-motion";
|
||||||
|
|
||||||
|
|
@ -11,7 +12,23 @@ function Home () {
|
||||||
useEffect(()=> {
|
useEffect(()=> {
|
||||||
document.title = "PowerOn";
|
document.title = "PowerOn";
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const location = useLocation();
|
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 (
|
return (
|
||||||
<div className={styles.homeContainer}>
|
<div className={styles.homeContainer}>
|
||||||
<div className={styles.body}>
|
<div className={styles.body}>
|
||||||
|
|
@ -19,21 +36,14 @@ function Home () {
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.homeContent}>
|
<div className={styles.homeContent}>
|
||||||
<AnimatePresence mode="wait">
|
<PageManager
|
||||||
<motion.div
|
loadingComponent={LoadingComponent}
|
||||||
key={location.pathname}
|
errorComponent={ErrorComponent}
|
||||||
initial={{ opacity: 0 }}
|
/>
|
||||||
animate={{ opacity: 1 }}
|
|
||||||
transition={{ duration: 0.3, ease: "easeInOut" }}
|
|
||||||
style={{ height: "100%", display: "flex", flexDirection: "column" }}
|
|
||||||
>
|
|
||||||
<Outlet />
|
|
||||||
</motion.div>
|
|
||||||
</AnimatePresence>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>)
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Home;
|
export default Home;
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,9 @@
|
||||||
.dashboardContainer {
|
.dashboardContentArea {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 20px;
|
|
||||||
font-family: var(--font-family);
|
|
||||||
width: 100%;
|
|
||||||
height: 100vh;
|
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chatLogContainer {
|
.chatLogContainer {
|
||||||
|
|
@ -13,6 +11,7 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chatArea {
|
.chatArea {
|
||||||
|
|
@ -20,5 +19,22 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,3 +38,28 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
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 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 MitgliederItem from '../../components/Mitglieder/MitgliederItem';
|
||||||
import { IoPersonAddSharp } from "react-icons/io5";
|
import { IoPersonAddSharp } from "react-icons/io5";
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { IoIosRefresh, IoIosLink } from 'react-icons/io';
|
import { IoIosRefresh, IoIosLink } from 'react-icons/io';
|
||||||
import { useLanguage } from '../../contexts/LanguageContext';
|
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 styles from './HomeStyles/TestSharepoint.module.css'
|
||||||
import { TestSharepointTable, useTestSharepointLogic } from '../../components/TestSharepoint'
|
import { TestSharepointTable, useTestSharepointLogic } from '../../components/TestSharepoint'
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import styles from './HomeStyles/Workflows.module.css'
|
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 { WorkflowsTable } from '../../components/Workflows'
|
||||||
import { useLanguage } from '../../contexts/LanguageContext'
|
import { useLanguage } from '../../contexts/LanguageContext'
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue