new dashboard

This commit is contained in:
idittrich-valueon 2025-07-23 10:37:07 +02:00
parent 8784373e76
commit 84764f932b
38 changed files with 3462 additions and 1087 deletions

7
.gitignore vendored
View file

@ -26,4 +26,9 @@ dist-ssr
# Environment files # Environment files
.env .env
.env.local .env.local
.env.*.local <<<<<<< Updated upstream
.env.*.local
=======
.env.*.local
>>>>>>> Stashed changes

View file

@ -1,16 +1,16 @@
{ {
"name": "frontend", "name": "frontend_nyla_new",
"private": true, "private": true,
"version": "0.0.0", "version": "0.0.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite --port 5176", "dev": "vite --port 5176",
"build": "vite build", "build": "tsc -b && vite build",
"lint": "eslint .", "lint": "eslint .",
"preview": "vite preview", "preview": "vite preview"
"start": "node server.js"
}, },
"dependencies": { "dependencies": {
<<<<<<< Updated upstream
"@azure/msal-browser": "^4.12.0", "@azure/msal-browser": "^4.12.0",
"@azure/msal-react": "^3.0.12", "@azure/msal-react": "^3.0.12",
"@xstate/react": "^5.0.0", "@xstate/react": "^5.0.0",
@ -24,23 +24,22 @@
"jwt-decode": "^4.0.0", "jwt-decode": "^4.0.0",
"motion": "^12.7.3", "motion": "^12.7.3",
"pg": "^8.8.0", "pg": "^8.8.0",
=======
>>>>>>> Stashed changes
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0"
"react-dropzone": "^14.3.8",
"react-icons": "^5.5.0",
"react-markdown": "^10.1.0",
"react-router-dom": "^7.5.0",
"xstate": "^5.18.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.21.0", "@eslint/js": "^9.30.1",
"@types/react": "^19.1.6", "@types/react": "^19.1.8",
"@types/react-dom": "^19.1.5", "@types/react-dom": "^19.1.6",
"@vitejs/plugin-react": "^4.3.4", "@vitejs/plugin-react": "^4.6.0",
"eslint": "^9.21.0", "eslint": "^9.30.1",
"eslint-plugin-react-hooks": "^5.1.0", "eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.19", "eslint-plugin-react-refresh": "^0.4.20",
"globals": "^15.15.0", "globals": "^16.3.0",
"vite": "^6.2.0" "typescript": "~5.8.3",
"typescript-eslint": "^8.35.1",
"vite": "^5.4.10"
} }
} }

View file

@ -1,15 +1,15 @@
:root { :root {
--color-bg: #FFFFFF; --color-bg: #F8F9FA; /* war vorher surface */
--color-surface: #F8F9FA; --color-surface: #EFEDE5; /* war vorher bg */
--color-text: #24262B; --color-text: #181818;
--color-primary: #8F00FF; --color-primary: #C7C5B2;
--color-primary-hover: #A020FF; --color-primary-hover: #D9D7C6;
--color-primary-disabled: #D1A6F9; --color-primary-disabled: #E3E2D8;
--color-secondary: #3F51B5; --color-secondary: #F25843;
--color-secondary-hover: #5A6CE0; --color-secondary-hover: #FF6A55;
--color-secondary-disabled: #BEC5EB; --color-secondary-disabled: #F5B0A4;
--color-red: #D85B65; --color-red: #D85B65;
--color-red-hover: #E77A81; --color-red-hover: #E77A81;
@ -19,26 +19,26 @@
--color-secondary-red-hover: #D46872; --color-secondary-red-hover: #D46872;
--color-secondary-red-disabled: #E8B7BA; --color-secondary-red-disabled: #E8B7BA;
--color-gray: #6C757D; --color-gray: #181818;
--color-gray-hover: #8A9299; --color-gray-hover: #2A2A2A;
--color-gray-disabled: #D6D8DB; --color-gray-disabled: #9B9B9B;
--font-family: "Trebuchet MS", sans-serif; --font-family: "DM Sans", sans-serif;
} }
/* Dark theme overrides */ /* Dark theme overrides */
.dark-theme { .dark-theme {
--color-bg: #121212; --color-bg: #181818; /* war vorher surface */
--color-surface: #1E1E1E; --color-surface: #1E1D1A; /* war vorher bg */
--color-text: #E5E7EB; --color-text: #E5E7EB;
--color-primary: #B266FF; --color-primary: #C7C5B2;
--color-primary-hover: #C68AFF; --color-primary-hover: #E0DECC;
--color-primary-disabled: #5C2B80; --color-primary-disabled: #59584F;
--color-secondary: #6F7BE5; --color-secondary: #F25843;
--color-secondary-hover: #8592FF; --color-secondary-hover: #FF715C;
--color-secondary-disabled: #3B4370; --color-secondary-disabled: #6E3E36;
--color-red: #FF6F7A; --color-red: #FF6F7A;
--color-red-hover: #FF8B94; --color-red-hover: #FF8B94;
@ -48,8 +48,8 @@
--color-secondary-red-hover: #E17683; --color-secondary-red-hover: #E17683;
--color-secondary-red-disabled: #70363C; --color-secondary-red-disabled: #70363C;
--color-gray: #A0A4AA; --color-gray: #181818;
--color-gray-hover: #C4C8CD; --color-gray-hover: #2E2E2E;
--color-gray-disabled: #505357; --color-gray-disabled: #505050;
} }

View file

@ -1,317 +1,16 @@
.dashboard_chat { .dashboard_chat {
display: flex; display: flex;
padding: 20px; padding: 20px;
flex-direction: column; flex-direction: column; /* Fixed: was 'space-between' which is invalid */
align-self: stretch; align-self: stretch;
border-radius: 30px;
background: var(--color-bg); background: var(--color-bg);
position: relative; position: relative;
box-shadow: 0px 2px 6px 0px rgba(194, 194, 194, 0.10); box-shadow: 0px 2px 6px 0px rgba(194, 194, 194, 0.10);
height: 100%;
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);
} }
.dashboard_chat.expanded {
width: 100%;
}
.chat_header {
display: flex;
justify-content: space-between;
align-items: flex-start;
flex-shrink: 0;
}
.chat_button_div {
display: flex;
gap: 20px;
align-items: flex-start;
}
.buttonWrapper {
display: flex;
flex-direction: column;
position: relative;
}
.chat_button {
text-align: center;
font-size: 14px;
font-style: normal;
font-weight: 500;
line-height: normal;
border: none;
background: none;
outline: none;
cursor: pointer;
padding: 0;
transition: all 0.2s ease;
font-family: var(--font-family);
}
.chat_button_active {
color: var(--color-text);
}
.chat_button_inactive {
color: var(--color-gray);
}
.chat_button_collapsed {
opacity: 50%;
color: var(--color-gray);
}
.iconContainer {
display: flex;
gap: 10px;
align-items: center;
}
.expandIcon, .collapseIcon {
cursor: pointer;
display: flex;
align-items: center;
color: var(--color-secondary);
}
.expandIcon:hover, .collapseIcon:hover {
color: var(--color-secondary-hover);
}
.horizontalLine {
width: 100%;
background-color: var(--color-text);
height: 1px;
margin-top: 20px;
}
.horizontalLineLight {
width: calc(100%);
background-color: var(--color-gray-disabled);
height: 2px;
margin-top: 39px;
margin-left: -20px;
position: absolute;
flex-shrink: 0;
}
.chat_content {
display: flex;
flex-direction: column;
flex: 1;
margin-top: 20px;
min-height: 0;
overflow: hidden;
}
.chat_messages {
flex: 1;
padding: 15px;
border-radius: 15px;
margin-bottom: 15px;
min-height: 200px;
overflow-y: auto;
overflow-x: hidden;
scroll-behavior: smooth;
}
.chat_input {
display: flex;
gap: 10px;
align-items: center;
flex-shrink: 0;
}
.message_input {
flex: 1;
padding: 12px 16px;
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-primary);
}
.send_button {
padding: 12px 12px;
background-color: var(--color-secondary);
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);
}
.send_button_icon {
height: 100%;
width: 100%;
margin: none;
padding: none;
}
.send_button:disabled {
background-color: var(--color-gray-disabled);
cursor: not-allowed;
opacity: 0.6;
}
.message_input:disabled {
background-color: var(--color-surface);
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 {
padding: 8px 12px;
background-color: var(--color-secondary-disabled);
border-left: 4px solid var(--color-secondary);
border-radius: 4px;
margin-bottom: 10px;
}
.workflow_status p {
margin: 0;
color: var(--color-secondary);
font-size: 13px;
font-style: italic;
font-family: var(--font-family);
}
.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);
}

View file

@ -48,91 +48,15 @@ const DashboardChat: React.FC<DashboardChatProps> = ({
}; };
return ( return (
<motion.div <div className={styles.dashboard_chat}>
className={`${styles.dashboard_chat} ${isExpanded ? styles.expanded : ''}`} <DashboardChatArea
layout selectedPrompt={selectedPrompt}
transition={{ duration: 0.4, ease: "easeOut" }} onPromptUsed={onPromptUsed}
> onWorkflowIdChange={onWorkflowIdChange}
<motion.div onWorkflowCompletedChange={onWorkflowCompletedChange}
className={styles.chat_header} resumeWorkflowId={resumeWorkflowId}
layout />
transition={{ duration: 0.3, ease: "easeOut" }} </div>
>
<div className={styles.chat_button_div}>
{[t('dashboard.chat.area'), t('dashboard.chat.history')].map((tab) => (
<div key={tab} className={styles.buttonWrapper}>
<motion.button
key={tab}
className={`${styles.chat_button} ${
activeTab === tab ? styles.chat_button_active : styles.chat_button_inactive
}`}
onClick={() => setActiveTab(tab)}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.98 }}
>
{tab}
</motion.button>
<AnimatePresence>
{activeTab === tab && (
<motion.div
className={styles.horizontalLine}
initial={{ opacity: 0, width: "0%" }}
animate={{ opacity: 1, width: "100%" }}
exit={{ opacity: 0, width: "0%" }}
transition={{ duration: 0.3, ease: "easeOut" }}
></motion.div>
)}
</AnimatePresence>
</div>
))}
</div>
<div className={styles.iconContainer}>
<motion.div
className={styles.expandIcon}
onClick={onToggleExpand}
whileTap={{ scale: 0.9 }}
whileHover={{ scale: 1.15 }}
animate={{ rotate: isExpanded ? 180 : 0 }}
transition={{ duration: 0.3, ease: "easeOut" }}
>
{isExpanded ? <BsArrowsAngleContract size={20} /> : <BsArrowsAngleExpand size={20} />}
</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.chat_content}
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
transition={{
duration: 0.4,
ease: [0.4, 0.0, 0.2, 1],
height: { duration: 0.4 },
opacity: { duration: 0.3 }
}}
style={{ overflow: "hidden" }}
>
{activeTab === t('dashboard.chat.area') ? (
<DashboardChatArea
selectedPrompt={selectedPrompt}
onPromptUsed={onPromptUsed}
onWorkflowIdChange={onWorkflowIdChange}
onWorkflowCompletedChange={onWorkflowCompletedChange}
resumeWorkflowId={resumeWorkflowId}
/>
) : (
<DashboardChatHistory
onWorkflowResume={handleWorkflowResume}
/>
)}
</motion.div>
</motion.div>
); );
}; };

View file

@ -1,10 +1,10 @@
import React, { useEffect } from "react"; import React, { useState } from "react";
import { DashboardChatAreaProps } from "./dashboardChatAreaTypes";
import { useChatLogic } from "./dashboardChatAreaLogic";
import { useLanguage } from "../../../../contexts/LanguageContext";
import MessageList from "./DashboardChatAreaMessageList"; import MessageList from "./DashboardChatAreaMessageList";
import ChatInput from "./DashboardChatAreaInput"; import FilePreview from "./DashboardChatAreaFilePreview";
import styles from './DashboardChatArea.module.css'; import InputArea from "./DashboardChatAreaInput";
import ConnectedFiles from "./DashboardChatAreaConnectedFiles";
import "./DashboardChatAreaStyles/grid.css";
import { DashboardChatAreaProps } from "./dashboardChatAreaTypes";
const DashboardChatArea: React.FC<DashboardChatAreaProps> = ({ const DashboardChatArea: React.FC<DashboardChatAreaProps> = ({
selectedPrompt, selectedPrompt,
@ -13,92 +13,134 @@ const DashboardChatArea: React.FC<DashboardChatAreaProps> = ({
onWorkflowCompletedChange, onWorkflowCompletedChange,
resumeWorkflowId resumeWorkflowId
}) => { }) => {
const { // Grid sizing state
// State const [horizontalSplit, setHorizontalSplit] = useState(60); // percentage
inputValue, const [verticalSplit, setVerticalSplit] = useState(60); // percentage
setInputValue, const [isDragging, setIsDragging] = useState<'horizontal' | 'vertical' | null>(null);
currentWorkflowId,
workflowCompleted,
attachedFiles,
// Refs
inputRef,
messagesEndRef,
// Data from hooks
messages,
messagesLoading,
messagesError,
startingWorkflow,
startError,
workflowStatus,
// Handlers
handleSend,
handleKeyPress,
startNewWorkflow,
handleStopWorkflow,
handleFileAttach,
handleFileRemove,
handleFilesSelect,
handleRetry,
// Workflow state
isWorkflowRunning,
isStoppingWorkflow,
shouldShowRetryButton
} = useChatLogic({
selectedPrompt,
onPromptUsed,
onWorkflowIdChange,
resumeWorkflowId
});
const { t } = useLanguage(); // File selection state
const [selectedFile, setSelectedFile] = useState<any>(null);
const [attachedFiles, setAttachedFiles] = useState<any[]>([]);
// Workflow state
const [currentWorkflowId, setCurrentWorkflowId] = useState<string | null>(resumeWorkflowId || null);
// Notify parent component when workflow completion status changes // Handle workflow ID changes
useEffect(() => { const handleWorkflowIdChange = (workflowId: string | null) => {
if (onWorkflowCompletedChange) { setCurrentWorkflowId(workflowId);
onWorkflowCompletedChange(workflowCompleted); if (onWorkflowIdChange) {
onWorkflowIdChange(workflowId);
} }
}, [workflowCompleted, onWorkflowCompletedChange]); };
const placeholder = workflowCompleted ? t('chat.continue_conversation') : t('chat.enter_message'); // Handle resizing
const handleMouseDown = (direction: 'horizontal' | 'vertical') => (e: React.MouseEvent) => {
e.preventDefault();
setIsDragging(direction);
};
const handleMouseMove = (e: MouseEvent) => {
if (!isDragging) return;
const container = document.querySelector('.chat-grid') as HTMLElement;
if (!container) return;
const rect = container.getBoundingClientRect();
if (isDragging === 'horizontal') {
const newSplit = ((e.clientY - rect.top) / rect.height) * 100;
setHorizontalSplit(Math.max(20, Math.min(80, newSplit)));
} else if (isDragging === 'vertical') {
const newSplit = ((e.clientX - rect.left) / rect.width) * 100;
setVerticalSplit(Math.max(20, Math.min(80, newSplit)));
}
};
const handleMouseUp = () => {
setIsDragging(null);
};
// Event listeners
React.useEffect(() => {
if (isDragging) {
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
document.body.style.cursor = isDragging === 'horizontal' ? 'ns-resize' : 'ew-resize';
document.body.style.userSelect = 'none';
return () => {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
document.body.style.cursor = '';
document.body.style.userSelect = '';
};
}
}, [isDragging]);
return ( return (
<div className={styles.chat_area}> <div
<MessageList className="chat-grid"
messages={messages} style={{
currentWorkflowId={currentWorkflowId} gridTemplateRows: `${horizontalSplit}% 1px ${100 - horizontalSplit}%`,
workflowStatus={workflowStatus} gridTemplateColumns: `${verticalSplit}% 1px ${100 - verticalSplit}%`
workflowCompleted={workflowCompleted} }}
startingWorkflow={startingWorkflow} >
startError={startError} {/* Top Left: Message List */}
messagesError={messagesError} <div className="quadrant messages-quadrant">
messagesLoading={messagesLoading} <MessageList
onStartNewWorkflow={startNewWorkflow} selectedPrompt={selectedPrompt}
messagesEndRef={messagesEndRef} onPromptUsed={onPromptUsed}
handleRetry={handleRetry} resumeWorkflowId={currentWorkflowId}
shouldShowRetryButton={shouldShowRetryButton} onFilePreview={setSelectedFile}
/>
</div>
{/* Vertical Divider */}
<div
className="divider vertical-divider"
onMouseDown={handleMouseDown('vertical')}
/> />
<ChatInput
inputValue={inputValue} {/* Top Right: File Preview */}
setInputValue={setInputValue} <div className="quadrant file-preview-quadrant">
onSend={handleSend} <FilePreview selectedFile={selectedFile} />
onKeyPress={handleKeyPress} </div>
isDisabled={startingWorkflow}
placeholder={placeholder} {/* Horizontal Divider */}
inputRef={inputRef} <div
isWorkflowRunning={isWorkflowRunning} className="divider horizontal-divider"
onStopWorkflow={handleStopWorkflow} onMouseDown={handleMouseDown('horizontal')}
isStoppingWorkflow={isStoppingWorkflow}
attachedFiles={attachedFiles}
onFileAttach={handleFileAttach}
onFileRemove={handleFileRemove}
onFilesSelect={handleFilesSelect}
/> />
{/* 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> </div>
); );
}; };
export default DashboardChatArea; export default DashboardChatArea;

View file

@ -0,0 +1,208 @@
import React, { useState, useEffect } from 'react';
import { useFileDownload } from '../../../../hooks/useWorkflows';
import { FileInfo } from './dashboardChatAreaTypes';
interface AttachedFile {
id: number;
name: string;
size: number;
type: string;
fileData?: File;
objectUrl?: string;
}
interface ConnectedFilesProps {
onFileSelect?: (file: FileInfo) => void;
selectedFile?: FileInfo | null;
attachedFiles?: AttachedFile[];
onRemoveFile?: (fileId: number) => void;
}
const ConnectedFiles: React.FC<ConnectedFilesProps> = ({
onFileSelect,
selectedFile,
attachedFiles = [],
onRemoveFile
}) => {
const [files, setFiles] = useState<FileInfo[]>([]);
const { downloadFile, isDownloading } = useFileDownload();
// Convert attached files to FileInfo format for compatibility with preview
const convertedAttachedFiles = attachedFiles.map(file => {
console.log('ConnectedFiles: Converting attached file:', file.name, 'Has fileData:', !!file.fileData, 'Has objectUrl:', !!file.objectUrl);
return {
id: file.id,
name: file.name,
mimeType: file.type,
size: file.size,
creationDate: new Date().toISOString(),
fileData: file.fileData,
objectUrl: file.objectUrl
};
});
// Combine attached files with workflow files
const allFiles = [...convertedAttachedFiles, ...files];
useEffect(() => {
// Could load workflow-specific files here in the future
}, []);
const getFileIcon = (mimeType: string) => {
if (mimeType.includes('pdf')) return '📄';
if (mimeType.includes('spreadsheet') || mimeType.includes('excel')) return '📊';
if (mimeType.startsWith('image/')) return '🖼️';
if (mimeType.startsWith('text/')) return '📝';
return '📎';
};
const formatFileSize = (bytes: number) => {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return Math.round(bytes / 1024) + ' KB';
return Math.round(bytes / (1024 * 1024)) + ' MB';
};
const handleFileClick = (file: any) => {
if (onFileSelect) {
console.log('ConnectedFiles: Selecting file:', file.name, 'Has fileData:', !!file.fileData, 'Has objectUrl:', !!file.objectUrl);
onFileSelect(file);
}
};
const handleDownload = async (file: FileInfo) => {
await downloadFile(file.id, file.name);
};
return (
<div style={{ padding: '16px', height: '100%', overflow: 'auto' }}>
<h3>Connected Files</h3>
{/* Show attached files count */}
{attachedFiles.length > 0 && (
<div style={{
marginBottom: '12px',
padding: '8px',
backgroundColor: '#e3f2fd',
borderRadius: '6px',
fontSize: '12px',
color: '#1976d2',
fontWeight: '500'
}}>
📎 {attachedFiles.length} file{attachedFiles.length !== 1 ? 's' : ''} attached for workflow
</div>
)}
{allFiles.length === 0 ? (
<p style={{ color: 'var(--color-gray)', textAlign: 'center' }}>
No files connected to this workflow
</p>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
{allFiles.map((file) => {
const isAttachedFile = attachedFiles.some(af => af.id === file.id);
return (
<div
key={file.id}
onClick={() => handleFileClick(file)}
style={{
padding: '12px',
border: `1px solid ${selectedFile?.id === file.id ? 'var(--color-secondary)' : 'var(--color-gray-disabled)'}`,
borderRadius: '8px',
cursor: 'pointer',
backgroundColor: selectedFile?.id === file.id ? 'var(--color-secondary-disabled)' : 'var(--color-bg)',
display: 'flex',
alignItems: 'center',
gap: '12px',
// Highlight attached files
...(isAttachedFile && {
borderColor: '#1976d2',
backgroundColor: '#f3f8ff'
})
}}
>
<span style={{ fontSize: '20px' }}>
{getFileIcon(file.mimeType)}
</span>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{
fontWeight: '500',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
display: 'flex',
alignItems: 'center',
gap: '6px'
}}>
{file.name}
{isAttachedFile && (
<span style={{
fontSize: '10px',
backgroundColor: '#1976d2',
color: 'white',
padding: '2px 6px',
borderRadius: '10px',
fontWeight: 'normal'
}}>
ATTACHED
</span>
)}
</div>
<div style={{
fontSize: '12px',
color: 'var(--color-gray)'
}}>
{file.size ? formatFileSize(file.size) : 'Unknown size'}
</div>
</div>
<div style={{ display: 'flex', gap: '4px' }}>
{isAttachedFile && onRemoveFile && (
<button
onClick={(e) => {
e.stopPropagation();
onRemoveFile(file.id);
}}
style={{
padding: '4px 8px',
fontSize: '12px',
backgroundColor: 'transparent',
border: '1px solid #ff6b6b',
borderRadius: '4px',
cursor: 'pointer',
color: '#ff6b6b'
}}
title="Remove from attachment"
>
</button>
)}
<button
onClick={(e) => {
e.stopPropagation();
handleDownload(file);
}}
disabled={isDownloading}
style={{
padding: '4px 8px',
fontSize: '12px',
backgroundColor: 'transparent',
border: '1px solid var(--color-gray-disabled)',
borderRadius: '4px',
cursor: 'pointer'
}}
title="Download file"
>
</button>
</div>
</div>
);
})}
</div>
)}
</div>
);
};
export default ConnectedFiles;

View file

@ -0,0 +1,192 @@
import React from 'react';
import { useFilePreview } from '../../../../hooks/useWorkflows';
import { FileInfo } from './dashboardChatAreaTypes';
interface AttachedFileWithData extends FileInfo {
fileData?: File;
objectUrl?: string;
}
interface FilePreviewProps {
selectedFile?: AttachedFileWithData | null;
}
const FilePreview: React.FC<FilePreviewProps> = ({ selectedFile }) => {
const { previewContent, fileMetadata, isLoading, error, fetchPreview } = useFilePreview();
const [imageUrl, setImageUrl] = React.useState<string | null>(null);
// Handle base64 image data from backend
React.useEffect(() => {
if (fileMetadata && fileMetadata.base64Encoded && fileMetadata.preview) {
const isImage = fileMetadata.mimeType?.startsWith('image/') ||
selectedFile?.name?.toLowerCase().match(/\.(png|jpg|jpeg|gif|bmp|webp)$/);
if (isImage) {
const dataUrl = `data:${fileMetadata.mimeType || 'image/png'};base64,${fileMetadata.preview}`;
setImageUrl(dataUrl);
}
}
}, [fileMetadata, selectedFile]);
React.useEffect(() => {
// Clean up previous object URL
const currentImageUrl = imageUrl;
if (currentImageUrl && currentImageUrl.startsWith('blob:')) {
URL.revokeObjectURL(currentImageUrl);
}
setImageUrl(null);
if (selectedFile?.id) {
// Check if it's an image file (either from mimeType or file extension)
const isImage = selectedFile.mimeType?.startsWith('image/') ||
selectedFile.name?.toLowerCase().match(/\.(png|jpg|jpeg|gif|bmp|webp)$/);
if (isImage) {
// If it's an attached file with file data, create object URL for preview
if (selectedFile.fileData) {
const url = URL.createObjectURL(selectedFile.fileData);
setImageUrl(url);
} else if (selectedFile.objectUrl) {
setImageUrl(selectedFile.objectUrl);
} else if (selectedFile.downloadUrl) {
setImageUrl(selectedFile.downloadUrl);
} else {
// For existing uploaded files, fetch the image data
fetchPreview(selectedFile.id);
}
} else {
// For non-image files, try to fetch preview
fetchPreview(selectedFile.id);
}
}
// Cleanup function
return () => {
if (currentImageUrl && currentImageUrl.startsWith('blob:')) {
URL.revokeObjectURL(currentImageUrl);
}
};
}, [selectedFile?.id, selectedFile?.fileData, selectedFile?.objectUrl]);
// Cleanup on unmount
React.useEffect(() => {
return () => {
if (imageUrl) {
URL.revokeObjectURL(imageUrl);
}
};
}, []);
const getFileType = (mimeType?: string) => {
if (!mimeType) return 'Unknown';
if (mimeType.startsWith('image/')) return 'Image';
if (mimeType.startsWith('text/')) return 'Text';
if (mimeType.includes('pdf')) return 'PDF';
if (mimeType.includes('spreadsheet') || mimeType.includes('excel')) return 'Spreadsheet';
return 'Document';
};
return (
<div style={{ padding: '16px', height: '100%', overflow: 'auto' }}>
<h3>File Preview</h3>
{!selectedFile && (
<p style={{ color: 'var(--color-gray)', textAlign: 'center' }}>
Select a file to preview
</p>
)}
{selectedFile && (
<div>
<div style={{
padding: '12px',
backgroundColor: 'var(--color-surface)',
borderRadius: '8px',
marginBottom: '16px'
}}>
<h4 style={{ margin: '0 0 8px 0' }}>{selectedFile.name}</h4>
<div style={{ fontSize: '12px', color: 'var(--color-gray)' }}>
Type: {getFileType(selectedFile.mimeType)}
Size: {selectedFile.size ? Math.round(selectedFile.size / 1024) + ' KB' : 'Unknown'}
</div>
</div>
{/* Image Preview - Show first for images */}
{(selectedFile.mimeType?.startsWith('image/') || selectedFile.name?.toLowerCase().match(/\.(png|jpg|jpeg|gif|bmp|webp)$/)) && imageUrl ? (
<div style={{ marginBottom: '16px' }}>
<img
src={imageUrl}
alt={selectedFile.name}
style={{
maxWidth: '100%',
maxHeight: '500px',
objectFit: 'contain',
borderRadius: '8px',
border: '1px solid var(--color-gray-disabled)',
display: 'block',
margin: '0 auto'
}}
onLoad={() => console.log('Image loaded successfully')}
onError={(e) => {
console.error('Image failed to load:', e);
console.log('Image URL:', imageUrl);
}}
/>
</div>
) : (selectedFile.mimeType?.startsWith('image/') || selectedFile.name?.toLowerCase().match(/\.(png|jpg|jpeg|gif|bmp|webp)$/)) ? (
<div style={{
padding: '20px',
textAlign: 'center',
backgroundColor: 'var(--color-surface)',
borderRadius: '8px',
marginBottom: '16px'
}}>
<p>🖼 Image preview loading...</p>
<small style={{ color: 'var(--color-gray)' }}>
Debug: imageUrl={imageUrl ? 'yes' : 'no'}, downloadUrl={selectedFile.downloadUrl ? 'yes' : 'no'},
fileData={selectedFile.fileData ? 'yes' : 'no'}, objectUrl={selectedFile.objectUrl ? 'yes' : 'no'}
<br />
MimeType: {selectedFile.mimeType}
</small>
</div>
) : null}
{/* Text/Code Preview - Only for non-images and when we don't have an image URL */}
{!(selectedFile.mimeType?.startsWith('image/') || selectedFile.name?.toLowerCase().match(/\.(png|jpg|jpeg|gif|bmp|webp)$/)) && !imageUrl && (
<>
{isLoading && <p>Loading preview...</p>}
{error && <p style={{ color: 'red' }}>Error: {error}</p>}
{previewContent && (
<div style={{
backgroundColor: 'var(--color-surface)',
padding: '16px',
borderRadius: '8px',
fontFamily: 'monospace',
fontSize: '12px',
lineHeight: '1.4',
whiteSpace: 'pre-wrap',
overflow: 'auto',
maxHeight: '400px'
}}>
{previewContent}
</div>
)}
{!previewContent && !isLoading && !error && (
<p style={{ color: 'var(--color-gray)' }}>
Preview not available for this file type
</p>
)}
</>
)}
</div>
)}
</div>
);
};
export default FilePreview;

View file

@ -1,239 +1,187 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from 'react';
import { motion } from "framer-motion"; import { useWorkflowOperations } from '../../../../hooks/useWorkflows';
import { LuSendHorizontal } from "react-icons/lu"; import { Prompt } from '../../../../hooks/usePrompts';
import { FaStop } from "react-icons/fa"; import FileAttachmentPopup from './FileAttachmentPopup';
import { IoAttach, IoClose } from "react-icons/io5";
import { ChatInputProps } from "./dashboardChatAreaTypes";
import { FileInfo } from "../../../../hooks/useFiles";
import { useLanguage } from "../../../../contexts/LanguageContext";
import DateienSelector from "../../../Dateien/DateienHinzufügen/DateienSelector";
import styles from './DashboardChatArea.module.css';
// Helper function to get file icon based on type interface InputAreaProps {
const getFileIcon = (mimeType?: string): string => { selectedPrompt?: Prompt | null;
if (!mimeType) return '📄'; onPromptUsed?: () => void;
onWorkflowIdChange?: (workflowId: string | null) => void;
const type = mimeType.toLowerCase(); onAttachedFilesChange?: (files: AttachedFile[]) => void;
attachedFiles?: AttachedFile[];
if (type.includes('image')) return '🖼️'; }
if (type.includes('video')) return '🎥';
if (type.includes('audio')) return '🎵';
if (type.includes('pdf')) return '📕';
if (type.includes('word') || type.includes('document')) return '📘';
if (type.includes('excel') || type.includes('spreadsheet')) return '📊';
if (type.includes('powerpoint') || type.includes('presentation')) return '📋';
if (type.includes('text')) return '📝';
if (type.includes('zip') || type.includes('archive')) return '📦';
if (type.includes('javascript') || type.includes('json') || type.includes('html') || type.includes('css')) return '💻';
return '📄';
};
const ChatInput: React.FC<ChatInputProps> = ({ interface AttachedFile {
inputValue, id: number;
setInputValue, name: string;
onSend, size: number;
onKeyPress, type: string;
isDisabled, fileData?: File;
placeholder, objectUrl?: string;
inputRef, }
isWorkflowRunning,
onStopWorkflow, const InputArea: React.FC<InputAreaProps> = ({
isStoppingWorkflow, selectedPrompt,
attachedFiles, onPromptUsed,
onFileAttach, onWorkflowIdChange,
onFileRemove, onAttachedFilesChange,
onFilesSelect attachedFiles: externalAttachedFiles = []
}) => { }) => {
const { t } = useLanguage(); const [inputValue, setInputValue] = useState('');
const [isUploadModalOpen, setIsUploadModalOpen] = useState(false); const [showFilePopup, setShowFilePopup] = useState(false);
const [isDragOver, setIsDragOver] = useState(false);
// Auto-resize textarea functionality // Always use external attached files from parent component
const currentAttachedFiles = externalAttachedFiles;
const { startWorkflow, startingWorkflow, startError } = useWorkflowOperations();
// Auto-fill input when prompt is selected
useEffect(() => { useEffect(() => {
if (inputRef?.current) { if (selectedPrompt) {
const textarea = inputRef.current; setInputValue(selectedPrompt.content);
textarea.style.height = 'auto';
// Calculate line height - approximately 1.5em per line
const lineHeight = 24; // Adjust this value based on your CSS line-height
const maxHeight = lineHeight * 8; // 8 lines maximum
const newHeight = Math.min(textarea.scrollHeight, maxHeight);
textarea.style.height = `${newHeight}px`;
// Enable/disable scroll based on content height
if (textarea.scrollHeight > maxHeight) {
textarea.style.overflowY = 'auto';
} else {
textarea.style.overflowY = 'hidden';
}
} }
}, [inputValue, inputRef]); }, [selectedPrompt]);
const handleAttachmentClick = () => { const handleSend = async () => {
setIsUploadModalOpen(true); if (!inputValue.trim() || startingWorkflow) return;
try {
const result = await startWorkflow({
prompt: inputValue,
listFileId: currentAttachedFiles.map(f => f.id)
});
if (result.success) {
setInputValue('');
if (onAttachedFilesChange) {
onAttachedFilesChange([]);
}
if (onPromptUsed) onPromptUsed();
if (onWorkflowIdChange && result.data?.id) {
onWorkflowIdChange(result.data.id);
}
}
} catch (error) {
console.error('Failed to start workflow:', error);
}
}; };
const handleFilesSelected = (files: FileInfo[]) => { const handleKeyPress = (e: React.KeyboardEvent) => {
onFilesSelect(files);
setIsUploadModalOpen(false);
};
const handleFileRemove = (fileId: number) => {
onFileRemove(fileId);
};
// Handle Enter key press for sending message (without Shift)
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Enter' && !e.shiftKey) { if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault(); e.preventDefault();
if (!isDisabled && (inputValue.trim() || attachedFiles.length > 0)) { handleSend();
onSend();
}
}
// Call original onKeyPress if it exists (for backward compatibility)
if (onKeyPress && e.key !== 'Enter') {
onKeyPress(e as any);
} }
}; };
// Drag and drop handlers const handleFilesAttached = (files: AttachedFile[]) => {
const handleDragEnter = (e: React.DragEvent) => { setShowFilePopup(false);
e.preventDefault(); if (onAttachedFilesChange) {
e.stopPropagation(); onAttachedFilesChange(files);
if (!isDisabled && !isWorkflowRunning) {
setIsDragOver(true);
} }
}; };
const handleDragOver = (e: React.DragEvent) => { const formatFileSize = (bytes: number): string => {
e.preventDefault(); if (bytes < 1024) return bytes + ' B';
e.stopPropagation(); if (bytes < 1024 * 1024) return Math.round(bytes / 1024) + ' KB';
}; return Math.round(bytes / (1024 * 1024)) + ' MB';
const handleDragLeave = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
// Only set drag over to false if we're leaving the entire input area
if (!e.currentTarget.contains(e.relatedTarget as Node)) {
setIsDragOver(false);
}
};
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
setIsDragOver(false);
if (isDisabled || isWorkflowRunning) {
return;
}
const files = Array.from(e.dataTransfer.files);
if (files.length > 0) {
// Convert File objects to FileInfo objects
const fileInfos: FileInfo[] = files.map((file, index) => ({
id: Date.now() + index, // Generate unique IDs
name: file.name,
mimeType: file.type,
size: file.size,
creationDate: new Date().toISOString(),
source: 'user_uploaded'
}));
onFilesSelect(fileInfos);
}
}; };
return ( return (
<motion.div <div style={{ padding: '16px', height: '100%', display: 'flex', flexDirection: 'column' }}>
className={`${styles.chat_input} ${isDragOver ? styles.drag_over : ''}`} <h3>Input</h3>
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} {startError && (
transition={{ delay: 0.3, duration: 0.3, ease: "easeOut" }} <div style={{
onDragEnter={handleDragEnter} padding: '8px',
onDragOver={handleDragOver} backgroundColor: '#ffe6e6',
onDragLeave={handleDragLeave} color: '#d00',
onDrop={handleDrop} borderRadius: '4px',
> marginBottom: '12px'
{/* Show attached files if any */} }}>
{attachedFiles.length > 0 && ( Error: {startError}
<div className={styles.attached_files}>
{attachedFiles.map((file) => (
<div key={file.id} className={styles.attached_file}>
<span className={styles.attached_file_icon}>
{getFileIcon(file.mimeType)}
</span>
<span className={styles.attached_file_name}>
{file.name}
</span>
<button
className={styles.attached_file_remove}
onClick={() => handleFileRemove(file.id)}
title={t('chat.remove_file')}
>
<IoClose size={12} />
</button>
</div>
))}
</div> </div>
)} )}
{/* Input row with text input, attachment button, and send button */} {/* Show attached files count */}
<div className={styles.input_row}> {currentAttachedFiles.length > 0 && (
<textarea <div style={{
ref={inputRef} marginBottom: '8px',
padding: '6px 10px',
backgroundColor: '#e3f2fd',
borderRadius: '4px',
fontSize: '12px',
color: '#1976d2',
textAlign: 'center'
}}>
📎 {currentAttachedFiles.length} file{currentAttachedFiles.length !== 1 ? 's' : ''} attached
</div>
)}
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px', flex: 1 }}>
<textarea
value={inputValue} value={inputValue}
onChange={(e) => setInputValue(e.target.value)} onChange={(e) => setInputValue(e.target.value)}
onKeyDown={handleKeyDown} onKeyPress={handleKeyPress}
placeholder={placeholder} placeholder="Enter your message or prompt..."
className={styles.message_input} disabled={startingWorkflow}
disabled={isDisabled}
rows={1}
style={{ style={{
flex: 1,
padding: '12px',
border: '1px solid var(--color-gray-disabled)',
borderRadius: '8px',
resize: 'none', resize: 'none',
minHeight: '24px', fontSize: '14px',
lineHeight: '24px' fontFamily: 'inherit'
}} }}
/> />
{/* Attachment button */} <div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
<motion.button <button
className={styles.attachment_button} onClick={() => setShowFilePopup(true)}
onClick={handleAttachmentClick} style={{
disabled={isDisabled || isWorkflowRunning} padding: '8px 12px',
whileTap={{ scale: 0.95 }} backgroundColor: 'var(--color-surface)',
transition={{ duration: 0.2, ease: "easeOut" }} border: '1px solid var(--color-gray-disabled)',
title={t('chat.attach_file')} borderRadius: '6px',
> cursor: 'pointer',
<IoAttach size={26} /> fontSize: '14px'
</motion.button> }}
>
{/* Send/Stop button */} 📎 Attach Files
<motion.button </button>
className={isWorkflowRunning ? styles.stop_button : styles.send_button}
onClick={isWorkflowRunning ? onStopWorkflow : onSend} <button
disabled={isWorkflowRunning ? isStoppingWorkflow : (isDisabled || (!inputValue.trim() && attachedFiles.length === 0))} onClick={handleSend}
whileTap={{ scale: 0.95 }} disabled={!inputValue.trim() || startingWorkflow}
transition={{ duration: 0.2, ease: "easeOut" }} style={{
> padding: '8px 16px',
{isWorkflowRunning ? ( backgroundColor: startingWorkflow ? 'var(--color-gray-disabled)' : 'var(--color-secondary)',
<FaStop className={styles.send_button_icon}/> color: 'white',
) : ( border: 'none',
<LuSendHorizontal className={styles.send_button_icon}/> borderRadius: '6px',
cursor: startingWorkflow ? 'not-allowed' : 'pointer'
}}
>
{startingWorkflow ? 'Starting...' : 'Send'}
</button>
{selectedPrompt && (
<span style={{ fontSize: '12px', color: 'var(--color-gray)' }}>
Using prompt: {selectedPrompt.name}
</span>
)} )}
</motion.button> </div>
</div> </div>
{/* Upload Modal */} {/* File Attachment Popup */}
<DateienSelector {showFilePopup && (
isOpen={isUploadModalOpen} <FileAttachmentPopup
onClose={() => setIsUploadModalOpen(false)} onClose={() => setShowFilePopup(false)}
onFilesSelected={handleFilesSelected} onFilesSelected={handleFilesAttached}
/> currentAttachedFiles={currentAttachedFiles}
</motion.div> />
)}
</div>
); );
}; };
export default ChatInput; export default InputArea;

View file

@ -1,15 +1,14 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { FaDownload } from "react-icons/fa"; import { FaDownload } from "react-icons/fa";
import { MdOutlineRemoveRedEye } from "react-icons/md"; import { MdOutlineRemoveRedEye } from "react-icons/md";
import { Message, Document } from "./dashboardChatAreaTypes";
import FilePreviewPopup from "./FilePreviewPopup";
import { useFileDownload } from "../../../../hooks/useWorkflows"; import { useFileDownload } from "../../../../hooks/useWorkflows";
import { useLanguage } from "../../../../contexts/LanguageContext"; import { useLanguage } from "../../../../contexts/LanguageContext";
import styles from './DashboardChatArea.module.css'; import { Message, Document } from "./dashboardChatAreaTypes";
interface MessageItemProps { interface MessageItemProps {
message: Message; message: Message;
index: number; index: number;
onFilePreview?: (file: any) => void;
} }
// Helper function to format file size // Helper function to format file size
@ -62,15 +61,22 @@ const getFileIcon = (type?: string, ext?: string): string => {
return '📄'; return '📄';
}; };
const MessageItem: React.FC<MessageItemProps> = ({ message, index }) => { const MessageItem: React.FC<MessageItemProps> = ({ message, index, onFilePreview }) => {
const { t } = useLanguage(); const { t } = useLanguage();
const [previewDocument, setPreviewDocument] = useState<Document | null>(null);
const [isPreviewOpen, setIsPreviewOpen] = useState(false);
const { downloadFile, isDownloading, error: downloadError } = useFileDownload(); const { downloadFile, isDownloading, error: downloadError } = useFileDownload();
// Debug: Log what the MessageItem is receiving
console.log(`🎭 MessageItem rendering:`, {
messageId: message.id,
messageRole: message.role,
hasDocuments: !!(message.documents),
documentsArray: message.documents,
documentsLength: message.documents?.length || 0,
documentsCheck: message.documents && message.documents.length > 0
});
const handleDocumentClick = (document: Document) => { const handleDocumentClick = (document: Document) => {
console.log(`🖱️ Document clicked:`, document);
// If there's a downloadUrl, use it; otherwise try the url // If there's a downloadUrl, use it; otherwise try the url
const downloadLink = document.downloadUrl || document.url; const downloadLink = document.downloadUrl || document.url;
@ -83,109 +89,184 @@ const MessageItem: React.FC<MessageItemProps> = ({ message, index }) => {
const handlePreview = (document: Document, e: React.MouseEvent) => { const handlePreview = (document: Document, e: React.MouseEvent) => {
e.stopPropagation(); e.stopPropagation();
// Use fileId if available, otherwise try to use id as fallback console.log(`👁️ Preview requested for:`, document);
const fileId = document.fileId || document.id;
if (!fileId) { // Use fileId if available, otherwise try to use id as fallback
const fileId = document.fileId || parseInt(document.id || '0');
if (!fileId || isNaN(fileId)) {
console.error('❌ Invalid file ID for preview:', document);
return; return;
} }
setPreviewDocument(document); console.log('✅ MessageItem - Previewing file:', { fileId, document });
setIsPreviewOpen(true);
}; // Call the parent callback to show preview in the file preview quadrant
if (onFilePreview) {
const handleClosePreview = () => { onFilePreview({
setIsPreviewOpen(false); id: fileId.toString(),
setPreviewDocument(null); name: document.name,
mimeType: document.type || 'application/octet-stream',
size: document.size,
fileId: fileId
});
}
}; };
const handleDownload = async (document: Document, e: React.MouseEvent) => { const handleDownload = async (document: Document, e: React.MouseEvent) => {
e.stopPropagation(); e.stopPropagation();
console.log(`⬇️ Download requested for:`, document);
// Use fileId if available, otherwise try to use id as fallback // Use fileId if available, otherwise try to use id as fallback
const fileId = document.fileId || document.id; const fileId = document.fileId || parseInt(document.id || '0');
if (!fileId) { if (!fileId) {
console.error('❌ No file ID for download:', document);
return; return;
} }
// Construct filename with extension if available // Construct filename with extension if available
const fileName = document.ext ? `${document.name}.${document.ext}` : document.name; const fileName = document.ext ? `${document.name}.${document.ext}` : document.name;
console.log(`💾 Downloading file ${fileId} as "${fileName}"`);
await downloadFile(fileId, fileName); await downloadFile(fileId, fileName);
}; };
// Debug: Log document check before rendering
const hasDocuments = message.documents && message.documents.length > 0;
console.log(`🔍 About to check documents:`, {
hasDocuments: !!(message.documents),
documentsLength: message.documents?.length || 0,
willRenderFiles: hasDocuments
});
// Log if no documents
if (!hasDocuments) {
console.log(`📭 No documents to render for message ${message.id}`);
}
return ( return (
<div <div
key={message.id || index} style={{
className={`${styles.message} ${styles[`message_${message.role}`]}`} padding: '12px',
borderRadius: '8px',
backgroundColor: message.role === 'user'
? 'var(--color-secondary-disabled)'
: 'var(--color-surface)',
marginBottom: '8px'
}}
> >
<div className={styles.message_role}> <div style={{
{message.role === 'user' ? t('chat.you') : message.agentName} fontSize: '12px',
color: 'var(--color-gray)',
marginBottom: '4px',
fontWeight: '500'
}}>
{message.role === 'user' ? 'You' : message.agentName}
{message.timestamp && `${new Date(message.timestamp).toLocaleTimeString()}`}
</div> </div>
<div className={styles.message_content}>
<div style={{
lineHeight: '1.5',
whiteSpace: 'pre-wrap'
}}>
{message.content} {message.content}
</div> </div>
{message.documents && message.documents.length > 0 && ( {hasDocuments && (
<div className={styles.message_documents}> <div style={{
{message.documents.map((document, docIndex) => ( marginTop: '12px',
<div padding: '8px',
key={document.id || docIndex} backgroundColor: 'var(--color-bg)',
className={styles.document_item} borderRadius: '6px',
onClick={() => handleDocumentClick(document)} border: '1px solid var(--color-gray-disabled)'
title={`${t('chat.click_to_open')} ${document.name}`} }}>
> <div style={{
<span className={styles.document_icon}> fontSize: '12px',
{getFileIcon(document.type, document.ext)} color: 'var(--color-gray)',
</span> marginBottom: '8px',
<div className={styles.document_info}> fontWeight: '500'
<div className={styles.document_name}> }}>
{document.ext ? `${document.name}.${document.ext}` : document.name} 📎 Attached Files ({message.documents!.length})
</div> </div>
<div className={styles.document_meta}> <div>
{message.documents!.map((document, docIndex) => {
console.log(`📄 Rendering document ${docIndex + 1}:`, document);
return (
<div
key={document.id || docIndex}
style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
padding: '6px',
borderRadius: '4px',
backgroundColor: 'var(--color-surface)',
marginBottom: '4px',
cursor: 'pointer'
}}
onClick={() => handleDocumentClick(document)}
title={`Click to open ${document.name}`}
>
<span style={{ fontSize: '16px' }}>
{getFileIcon(document.type, document.ext)}
</span>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{
fontSize: '14px',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap'
}}>
{document.ext ? `${document.name}.${document.ext}` : document.name}
</div>
{document.size && ( {document.size && (
<span className={styles.document_size}> <div style={{
fontSize: '12px',
color: 'var(--color-gray)'
}}>
{formatFileSize(document.size)} {formatFileSize(document.size)}
</span> </div>
)} )}
</div> </div>
<div style={{ display: 'flex', gap: '4px' }}>
<button
onClick={(e) => handlePreview(document, e)}
style={{
padding: '4px 8px',
fontSize: '12px',
backgroundColor: 'transparent',
border: '1px solid var(--color-gray-disabled)',
borderRadius: '4px',
cursor: 'pointer'
}}
title="Preview file"
>
👁
</button>
<button
onClick={(e) => handleDownload(document, e)}
disabled={isDownloading}
style={{
padding: '4px 8px',
fontSize: '12px',
backgroundColor: 'transparent',
border: '1px solid var(--color-gray-disabled)',
borderRadius: '4px',
cursor: 'pointer'
}}
title="Download file"
>
</button>
</div>
</div> </div>
<div className={styles.document_actions}> );
<button })}
className={styles.document_action_button}
onClick={(e) => handlePreview(document, e)}
title={t('chat.preview_document')}
>
<MdOutlineRemoveRedEye />
</button>
<button
className={styles.document_action_button}
onClick={(e) => handleDownload(document, e)}
title={t('chat.download_document')}
>
<FaDownload />
</button>
</div>
</div>
))}
</div> </div>
)}
{message.timestamp && (
<div className={styles.message_timestamp}>
{new Date(message.timestamp).toLocaleTimeString()}
</div> </div>
)} )}
{/* File Preview Popup */}
{previewDocument && (
<FilePreviewPopup
document={previewDocument}
isOpen={isPreviewOpen}
onClose={handleClosePreview}
/>
)}
</div> </div>
); );
}; };

View file

@ -1,83 +1,255 @@
import React from "react"; import React, { useEffect, useRef, useState, useCallback } from 'react';
import { motion } from "framer-motion"; import { useWorkflowStatus } from '../../../../hooks/useWorkflows';
import { MessageListProps } from "./dashboardChatAreaTypes"; import { Prompt } from '../../../../hooks/usePrompts';
import MessageItem from "./DashboardChatAreaMessageItem"; import { useApiRequest } from '../../../../hooks/useApi';
import WorkflowStatusDisplay from "./DashbaordChatAreaStatusDisplay"; import MessageItem from './DashboardChatAreaMessageItem';
import { useLanguage } from "../../../../contexts/LanguageContext"; import { Message, Document, WorkflowMessage } from './dashboardChatAreaTypes';
import styles from './DashboardChatArea.module.css';
const MessageList: React.FC<MessageListProps> = ({ interface MessageListProps {
messages, selectedPrompt?: Prompt | null;
currentWorkflowId, onPromptUsed?: () => void;
workflowStatus, resumeWorkflowId?: string | null;
workflowCompleted, onFilePreview?: (file: any) => void;
startingWorkflow, }
startError,
messagesError, // Custom hook to fetch and transform messages like the old code
messagesLoading, const useTransformedMessages = (workflowId: string | null) => {
onStartNewWorkflow, const [messages, setMessages] = useState<Message[]>([]);
messagesEndRef, const [loading, setLoading] = useState(false);
handleRetry, const [error, setError] = useState<string | null>(null);
shouldShowRetryButton const { request } = useApiRequest();
const fetchMessages = useCallback(async () => {
if (!workflowId) {
setMessages([]);
return;
}
setLoading(true);
setError(null);
try {
console.log(`🔍 Fetching messages for workflow: ${workflowId}`);
// Fetch workflow messages
const workflowMessages: WorkflowMessage[] = await request({
url: `/api/workflows/${workflowId}/messages`,
method: 'get'
});
console.log(`📨 Received ${workflowMessages.length} messages from API:`, workflowMessages);
// Debug each message structure
workflowMessages.forEach((msg, index) => {
console.log(`📄 Message ${index + 1}:`, {
id: msg.id,
role: msg.role,
content: msg.content?.substring(0, 50) + '...',
fileIds: msg.fileIds,
hasFileIds: !!(msg.fileIds && msg.fileIds.length > 0),
fileIdsLength: msg.fileIds?.length || 0
});
});
// Transform each message
const transformedMessages = await Promise.all(
workflowMessages.map(async (workflowMessage: WorkflowMessage, msgIndex) => {
console.log(`🔄 Transforming message ${msgIndex + 1} (${workflowMessage.id})`);
let documents: Document[] = [];
// Fetch file metadata if fileIds exist
if (workflowMessage.fileIds && workflowMessage.fileIds.length > 0) {
console.log(`📎 Message ${workflowMessage.id} has ${workflowMessage.fileIds.length} fileIds:`, workflowMessage.fileIds);
const documentPromises = workflowMessage.fileIds.map(async (fileId, fileIndex) => {
try {
console.log(`📁 Fetching metadata for file ${fileIndex + 1}/${workflowMessage.fileIds!.length}: ${fileId}`);
const response = await request({
url: `/api/workflows/files/${fileId}/preview`,
method: 'get'
});
console.log(`✅ File ${fileId} metadata received:`, response);
const document: Document = {
id: fileId.toString(),
fileId: fileId,
name: response.name || response.fileName || `File_${fileId}`,
ext: response.extension || response.ext || (response.name ? response.name.split('.').pop() : 'txt'),
type: response.mimeType || response.type || 'application/octet-stream',
size: response.size || 0,
downloadUrl: response.downloadUrl || response.url
};
console.log(`🗂️ Created document object:`, document);
return document;
} catch (error) {
console.error(`❌ Failed to fetch metadata for file ${fileId}:`, error);
// Return a fallback object for failed requests
const fallbackDoc: Document = {
id: fileId.toString(),
fileId: fileId,
name: `File_${fileId}`,
ext: 'unknown',
type: 'application/octet-stream',
size: 0
};
console.log(`🔧 Created fallback document:`, fallbackDoc);
return fallbackDoc;
}
});
documents = await Promise.all(documentPromises);
console.log(`📋 All files processed for message ${workflowMessage.id}. Total documents: ${documents.length}`);
} else {
console.log(`📭 Message ${workflowMessage.id} has no fileIds`);
}
// Transform to old Message format
const message: Message = {
id: workflowMessage.id,
role: workflowMessage.role,
agentName: workflowMessage.role === 'user' ? 'You' : 'Assistant',
content: workflowMessage.content,
timestamp: workflowMessage.timestamp,
documents: documents
};
console.log(`✨ Final transformed message:`, {
id: message.id,
role: message.role,
documentsCount: message.documents?.length || 0,
hasDocuments: !!(message.documents && message.documents.length > 0)
});
return message;
})
);
console.log(`🎉 Successfully transformed all ${transformedMessages.length} messages`);
console.log(`📊 Summary:`, transformedMessages.map(msg => ({
id: msg.id,
role: msg.role,
documentsCount: msg.documents?.length || 0
})));
setMessages(transformedMessages);
} catch (err: any) {
console.error('💥 Error fetching messages:', err);
setError(err.message || 'Failed to fetch messages');
} finally {
setLoading(false);
}
}, [workflowId, request]);
useEffect(() => {
fetchMessages();
}, [fetchMessages]);
return { messages, loading, error, refetch: fetchMessages };
};
const MessageList: React.FC<MessageListProps> = ({
selectedPrompt,
onPromptUsed,
resumeWorkflowId,
onFilePreview
}) => { }) => {
const { t } = useLanguage(); const { messages, loading, error, refetch } = useTransformedMessages(resumeWorkflowId || null);
const { status } = useWorkflowStatus(resumeWorkflowId || null);
const intervalRef = useRef<number | null>(null);
const [isInitialLoad, setIsInitialLoad] = React.useState(true);
// Auto-refresh messages every 3 seconds when workflow is active
useEffect(() => {
if (resumeWorkflowId && status?.status &&
(status.status === 'running' || status.status === 'processing' || status.status === 'started')) {
intervalRef.current = window.setInterval(() => {
console.log('🔄 Auto-refreshing messages due to active workflow');
refetch();
}, 3000);
} else {
// Stop polling when completed, failed, or no workflow
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
}
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
};
}, [resumeWorkflowId, status?.status, refetch]);
// Initial load when workflow ID changes
useEffect(() => {
if (resumeWorkflowId) {
console.log(`🚀 Starting initial load for workflow: ${resumeWorkflowId}`);
setIsInitialLoad(true);
refetch().finally(() => setIsInitialLoad(false));
} else {
setIsInitialLoad(false);
}
}, [resumeWorkflowId, refetch]);
return ( return (
<motion.div <div style={{ padding: '16px', height: '100%', overflow: 'auto' }}>
className={styles.chat_messages} <h3>Messages</h3>
initial={{ opacity: 0 }}
animate={{ opacity: 1 }} {error && <p style={{ color: 'red' }}>Error: {error}</p>}
transition={{ delay: 0.2, duration: 0.3, ease: "easeOut" }}
> {status && (
<div className={styles.messages_container}> <div style={{
{startingWorkflow && ( padding: '8px',
<div className={styles.loading_message}> backgroundColor: 'var(--color-surface)',
<p>{workflowCompleted && currentWorkflowId ? t('chat.sending_followup', 'Sending follow-up message...') : t('chat.sending_message', 'Sending message...')}</p> borderRadius: '4px',
</div> marginBottom: '16px'
)} }}>
{startError && ( <strong>Status:</strong> {status.status}
<div className={styles.error_message}> {status.currentRound && ` (Round ${status.currentRound})`}
<p>{t('chat.error_prefix', 'Error:')} {startError}</p> {/* Show a small indicator when polling for updates */}
</div> {intervalRef.current && (
)} <span style={{
{messagesError && ( marginLeft: '8px',
<div className={styles.error_message}> fontSize: '12px',
<p>{t('chat.error_loading_messages', 'Error loading messages:')} {messagesError}</p> color: 'var(--color-secondary)',
</div> opacity: 0.7
)} }}>
{currentWorkflowId && messagesLoading && messages.length === 0 && ( 🔄 Live updates
<div className={styles.loading_message}> </span>
<p>{t('chat.loading_workflow_messages', 'Loading workflow messages...')}</p> )}
</div> </div>
)} )}
{messages.length > 0 ? (
messages.map((message, index) => ( <div style={{ display: 'flex', flexDirection: 'column' }}>
{messages.map((message, index) => {
console.log(`🎨 Rendering message ${message.id} with ${message.documents?.length || 0} documents`);
return (
<MessageItem <MessageItem
key={message.id || index} key={message.id}
message={message} message={message}
index={index} index={index}
onFilePreview={onFilePreview}
/> />
)) );
) : !currentWorkflowId ? ( })}
<p className={styles.placeholder_text}>{t('chat.start_conversation', 'Start a conversation by entering a message, selecting a template, or continuing a previous workflow...')}</p>
) : null}
{/* Spacer to push workflow status to bottom when there are fewer messages */}
{messages.length < 3 && <div className={styles.messages_spacer} />}
<WorkflowStatusDisplay
currentWorkflowId={currentWorkflowId}
workflowStatus={workflowStatus}
workflowCompleted={workflowCompleted}
onStartNewWorkflow={onStartNewWorkflow}
handleRetry={handleRetry}
shouldShowRetryButton={shouldShowRetryButton}
/>
<div ref={messagesEndRef} />
</div> </div>
</motion.div>
{loading && (
<div style={{ textAlign: 'center', padding: '16px' }}>
<p style={{ color: 'var(--color-gray)' }}>Loading messages...</p>
</div>
)}
{messages.length === 0 && !isInitialLoad && !loading && (
<p style={{ color: 'var(--color-gray)', textAlign: 'center' }}>
No messages yet. Start a workflow to see messages here.
</p>
)}
</div>
); );
}; };
export default MessageList; export default MessageList;

View file

@ -0,0 +1,104 @@
import React, { useEffect } from "react";
import { DashboardChatAreaProps } from "./dashboardChatAreaTypes";
import { useChatLogic } from "./dashboardChatAreaLogic";
import { useLanguage } from "../../../../contexts/LanguageContext";
import MessageList from "./DashboardChatAreaMessageList";
import ChatInput from "./DashboardChatAreaInput";
import styles from './DashboardChatArea.module.css';
const DashboardChatArea: React.FC<DashboardChatAreaProps> = ({
selectedPrompt,
onPromptUsed,
onWorkflowIdChange,
onWorkflowCompletedChange,
resumeWorkflowId
}) => {
const {
// State
inputValue,
setInputValue,
currentWorkflowId,
workflowCompleted,
attachedFiles,
// Refs
inputRef,
messagesEndRef,
// Data from hooks
messages,
messagesLoading,
messagesError,
startingWorkflow,
startError,
workflowStatus,
// Handlers
handleSend,
handleKeyPress,
startNewWorkflow,
handleStopWorkflow,
handleFileAttach,
handleFileRemove,
handleFilesSelect,
handleRetry,
// Workflow state
isWorkflowRunning,
isStoppingWorkflow,
shouldShowRetryButton
} = useChatLogic({
selectedPrompt,
onPromptUsed,
onWorkflowIdChange,
resumeWorkflowId
});
const { t } = useLanguage();
// Notify parent component when workflow completion status changes
useEffect(() => {
if (onWorkflowCompletedChange) {
onWorkflowCompletedChange(workflowCompleted);
}
}, [workflowCompleted, onWorkflowCompletedChange]);
const placeholder = workflowCompleted ? t('chat.continue_conversation') : t('chat.enter_message');
return (
<div className={styles.chat_area}>
<MessageList
messages={messages}
currentWorkflowId={currentWorkflowId}
workflowStatus={workflowStatus}
workflowCompleted={workflowCompleted}
startingWorkflow={startingWorkflow}
startError={startError}
messagesError={messagesError}
messagesLoading={messagesLoading}
onStartNewWorkflow={startNewWorkflow}
messagesEndRef={messagesEndRef}
handleRetry={handleRetry}
shouldShowRetryButton={shouldShowRetryButton}
/>
<ChatInput
inputValue={inputValue}
setInputValue={setInputValue}
onSend={handleSend}
onKeyPress={handleKeyPress}
isDisabled={startingWorkflow}
placeholder={placeholder}
inputRef={inputRef}
isWorkflowRunning={isWorkflowRunning}
onStopWorkflow={handleStopWorkflow}
isStoppingWorkflow={isStoppingWorkflow}
attachedFiles={attachedFiles}
onFileAttach={handleFileAttach}
onFileRemove={handleFileRemove}
onFilesSelect={handleFilesSelect}
/>
</div>
);
};
export default DashboardChatArea;

View file

@ -0,0 +1,239 @@
import React, { useState, useEffect } from "react";
import { motion } from "framer-motion";
import { LuSendHorizontal } from "react-icons/lu";
import { FaStop } from "react-icons/fa";
import { IoAttach, IoClose } from "react-icons/io5";
import { ChatInputProps } from "./dashboardChatAreaTypes";
import { FileInfo } from "../../../../hooks/useFiles";
import { useLanguage } from "../../../../contexts/LanguageContext";
import DateienSelector from "../../../Dateien/DateienHinzufügen/DateienSelector";
import styles from './DashboardChatArea.module.css';
// Helper function to get file icon based on type
const getFileIcon = (mimeType?: string): string => {
if (!mimeType) return '📄';
const type = mimeType.toLowerCase();
if (type.includes('image')) return '🖼️';
if (type.includes('video')) return '🎥';
if (type.includes('audio')) return '🎵';
if (type.includes('pdf')) return '📕';
if (type.includes('word') || type.includes('document')) return '📘';
if (type.includes('excel') || type.includes('spreadsheet')) return '📊';
if (type.includes('powerpoint') || type.includes('presentation')) return '📋';
if (type.includes('text')) return '📝';
if (type.includes('zip') || type.includes('archive')) return '📦';
if (type.includes('javascript') || type.includes('json') || type.includes('html') || type.includes('css')) return '💻';
return '📄';
};
const ChatInput: React.FC<ChatInputProps> = ({
inputValue,
setInputValue,
onSend,
onKeyPress,
isDisabled,
placeholder,
inputRef,
isWorkflowRunning,
onStopWorkflow,
isStoppingWorkflow,
attachedFiles,
onFileAttach,
onFileRemove,
onFilesSelect
}) => {
const { t } = useLanguage();
const [isUploadModalOpen, setIsUploadModalOpen] = useState(false);
const [isDragOver, setIsDragOver] = useState(false);
// Auto-resize textarea functionality
useEffect(() => {
if (inputRef?.current) {
const textarea = inputRef.current;
textarea.style.height = 'auto';
// Calculate line height - approximately 1.5em per line
const lineHeight = 24; // Adjust this value based on your CSS line-height
const maxHeight = lineHeight * 8; // 8 lines maximum
const newHeight = Math.min(textarea.scrollHeight, maxHeight);
textarea.style.height = `${newHeight}px`;
// Enable/disable scroll based on content height
if (textarea.scrollHeight > maxHeight) {
textarea.style.overflowY = 'auto';
} else {
textarea.style.overflowY = 'hidden';
}
}
}, [inputValue, inputRef]);
const handleAttachmentClick = () => {
setIsUploadModalOpen(true);
};
const handleFilesSelected = (files: FileInfo[]) => {
onFilesSelect(files);
setIsUploadModalOpen(false);
};
const handleFileRemove = (fileId: number) => {
onFileRemove(fileId);
};
// Handle Enter key press for sending message (without Shift)
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
if (!isDisabled && (inputValue.trim() || attachedFiles.length > 0)) {
onSend();
}
}
// Call original onKeyPress if it exists (for backward compatibility)
if (onKeyPress && e.key !== 'Enter') {
onKeyPress(e as any);
}
};
// Drag and drop handlers
const handleDragEnter = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
if (!isDisabled && !isWorkflowRunning) {
setIsDragOver(true);
}
};
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
};
const handleDragLeave = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
// Only set drag over to false if we're leaving the entire input area
if (!e.currentTarget.contains(e.relatedTarget as Node)) {
setIsDragOver(false);
}
};
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
setIsDragOver(false);
if (isDisabled || isWorkflowRunning) {
return;
}
const files = Array.from(e.dataTransfer.files);
if (files.length > 0) {
// Convert File objects to FileInfo objects
const fileInfos: FileInfo[] = files.map((file, index) => ({
id: Date.now() + index, // Generate unique IDs
name: file.name,
mimeType: file.type,
size: file.size,
creationDate: new Date().toISOString(),
source: 'user_uploaded'
}));
onFilesSelect(fileInfos);
}
};
return (
<motion.div
className={`${styles.chat_input} ${isDragOver ? styles.drag_over : ''}`}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3, duration: 0.3, ease: "easeOut" }}
onDragEnter={handleDragEnter}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
>
{/* Show attached files if any */}
{attachedFiles.length > 0 && (
<div className={styles.attached_files}>
{attachedFiles.map((file) => (
<div key={file.id} className={styles.attached_file}>
<span className={styles.attached_file_icon}>
{getFileIcon(file.mimeType)}
</span>
<span className={styles.attached_file_name}>
{file.name}
</span>
<button
className={styles.attached_file_remove}
onClick={() => handleFileRemove(file.id)}
title={t('chat.remove_file')}
>
<IoClose size={12} />
</button>
</div>
))}
</div>
)}
{/* Input row with text input, attachment button, and send button */}
<div className={styles.input_row}>
<textarea
ref={inputRef}
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={handleKeyDown}
placeholder={placeholder}
className={styles.message_input}
disabled={isDisabled}
rows={1}
style={{
resize: 'none',
minHeight: '24px',
lineHeight: '24px'
}}
/>
{/* Attachment button */}
<motion.button
className={styles.attachment_button}
onClick={handleAttachmentClick}
disabled={isDisabled || isWorkflowRunning}
whileTap={{ scale: 0.95 }}
transition={{ duration: 0.2, ease: "easeOut" }}
title={t('chat.attach_file')}
>
<IoAttach size={26} />
</motion.button>
{/* Send/Stop button */}
<motion.button
className={isWorkflowRunning ? styles.stop_button : styles.send_button}
onClick={isWorkflowRunning ? onStopWorkflow : onSend}
disabled={isWorkflowRunning ? isStoppingWorkflow : (isDisabled || (!inputValue.trim() && attachedFiles.length === 0))}
whileTap={{ scale: 0.95 }}
transition={{ duration: 0.2, ease: "easeOut" }}
>
{isWorkflowRunning ? (
<FaStop className={styles.send_button_icon}/>
) : (
<LuSendHorizontal className={styles.send_button_icon}/>
)}
</motion.button>
</div>
{/* Upload Modal */}
<DateienSelector
isOpen={isUploadModalOpen}
onClose={() => setIsUploadModalOpen(false)}
onFilesSelected={handleFilesSelected}
/>
</motion.div>
);
};
export default ChatInput;

View file

@ -0,0 +1,193 @@
import React, { useState } from "react";
import { FaDownload } from "react-icons/fa";
import { MdOutlineRemoveRedEye } from "react-icons/md";
import { Message, Document } from "./dashboardChatAreaTypes";
import FilePreviewPopup from "./FilePreviewPopup";
import { useFileDownload } from "../../../../hooks/useWorkflows";
import { useLanguage } from "../../../../contexts/LanguageContext";
import styles from './DashboardChatArea.module.css';
interface MessageItemProps {
message: Message;
index: number;
}
// Helper function to format file size
const formatFileSize = (bytes?: number): string => {
if (!bytes) return '';
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
};
// Helper function to get file icon based on type or extension
const getFileIcon = (type?: string, ext?: string): string => {
// Use extension first if available, then fall back to MIME type
const extension = ext?.toLowerCase();
const mimeType = type?.toLowerCase();
// Check extension first
if (extension) {
// Images
if (['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'].includes(extension)) return '🖼️';
// Videos
if (['mp4', 'avi', 'mov', 'wmv', 'flv', 'webm', 'mkv'].includes(extension)) return '🎥';
// Audio
if (['mp3', 'wav', 'aac', 'flac', 'ogg', 'wma'].includes(extension)) return '🎵';
// Documents
if (extension === 'pdf') return '📕';
if (['doc', 'docx'].includes(extension)) return '📘';
if (['xls', 'xlsx'].includes(extension)) return '📊';
if (['ppt', 'pptx'].includes(extension)) return '📋';
if (['txt', 'md', 'rtf'].includes(extension)) return '📝';
// Archives
if (['zip', 'rar', '7z', 'tar', 'gz'].includes(extension)) return '📦';
// Code files
if (['js', 'ts', 'jsx', 'tsx', 'html', 'css', 'py', 'java', 'cpp', 'c', 'php'].includes(extension)) return '💻';
}
// Fall back to MIME type if extension didn't match
if (mimeType) {
if (mimeType.includes('image')) return '🖼️';
if (mimeType.includes('video')) return '🎥';
if (mimeType.includes('audio')) return '🎵';
if (mimeType.includes('pdf')) return '📕';
if (mimeType.includes('text')) return '📝';
if (mimeType.includes('word') || mimeType.includes('document')) return '📘';
if (mimeType.includes('excel') || mimeType.includes('spreadsheet')) return '📊';
if (mimeType.includes('powerpoint') || mimeType.includes('presentation')) return '📋';
if (mimeType.includes('zip') || mimeType.includes('archive')) return '📦';
}
return '📄';
};
const MessageItem: React.FC<MessageItemProps> = ({ message, index }) => {
const { t } = useLanguage();
const [previewDocument, setPreviewDocument] = useState<Document | null>(null);
const [isPreviewOpen, setIsPreviewOpen] = useState(false);
const { downloadFile, isDownloading, error: downloadError } = useFileDownload();
const handleDocumentClick = (document: Document) => {
// If there's a downloadUrl, use it; otherwise try the url
const downloadLink = document.downloadUrl || document.url;
if (downloadLink) {
// Open the document in a new tab
window.open(downloadLink, '_blank');
}
};
const handlePreview = (document: Document, e: React.MouseEvent) => {
e.stopPropagation();
// Use fileId if available, otherwise try to use id as fallback
const fileId = document.fileId || document.id;
if (!fileId) {
return;
}
setPreviewDocument(document);
setIsPreviewOpen(true);
};
const handleClosePreview = () => {
setIsPreviewOpen(false);
setPreviewDocument(null);
};
const handleDownload = async (document: Document, e: React.MouseEvent) => {
e.stopPropagation();
// Use fileId if available, otherwise try to use id as fallback
const fileId = document.fileId || document.id;
if (!fileId) {
return;
}
// Construct filename with extension if available
const fileName = document.ext ? `${document.name}.${document.ext}` : document.name;
await downloadFile(fileId, fileName);
};
return (
<div
key={message.id || index}
className={`${styles.message} ${styles[`message_${message.role}`]}`}
>
<div className={styles.message_role}>
{message.role === 'user' ? t('chat.you') : message.agentName}
</div>
<div className={styles.message_content}>
{message.content}
</div>
{message.documents && message.documents.length > 0 && (
<div className={styles.message_documents}>
{message.documents.map((document, docIndex) => (
<div
key={document.id || docIndex}
className={styles.document_item}
onClick={() => handleDocumentClick(document)}
title={`${t('chat.click_to_open')} ${document.name}`}
>
<span className={styles.document_icon}>
{getFileIcon(document.type, document.ext)}
</span>
<div className={styles.document_info}>
<div className={styles.document_name}>
{document.ext ? `${document.name}.${document.ext}` : document.name}
</div>
<div className={styles.document_meta}>
{document.size && (
<span className={styles.document_size}>
{formatFileSize(document.size)}
</span>
)}
</div>
</div>
<div className={styles.document_actions}>
<button
className={styles.document_action_button}
onClick={(e) => handlePreview(document, e)}
title={t('chat.preview_document')}
>
<MdOutlineRemoveRedEye />
</button>
<button
className={styles.document_action_button}
onClick={(e) => handleDownload(document, e)}
title={t('chat.download_document')}
>
<FaDownload />
</button>
</div>
</div>
))}
</div>
)}
{message.timestamp && (
<div className={styles.message_timestamp}>
{new Date(message.timestamp).toLocaleTimeString()}
</div>
)}
{/* File Preview Popup */}
{previewDocument && (
<FilePreviewPopup
document={previewDocument}
isOpen={isPreviewOpen}
onClose={handleClosePreview}
/>
)}
</div>
);
};
export default MessageItem;

View file

@ -0,0 +1,83 @@
import React from "react";
import { motion } from "framer-motion";
import { MessageListProps } from "./dashboardChatAreaTypes";
import MessageItem from "./DashboardChatAreaMessageItem";
import WorkflowStatusDisplay from "./DashbaordChatAreaStatusDisplayold";
import { useLanguage } from "../../../../contexts/LanguageContext";
import styles from './DashboardChatArea.module.css';
const MessageList: React.FC<MessageListProps> = ({
messages,
currentWorkflowId,
workflowStatus,
workflowCompleted,
startingWorkflow,
startError,
messagesError,
messagesLoading,
onStartNewWorkflow,
messagesEndRef,
handleRetry,
shouldShowRetryButton
}) => {
const { t } = useLanguage();
return (
<motion.div
className={styles.chat_messages}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.2, duration: 0.3, ease: "easeOut" }}
>
<div className={styles.messages_container}>
{startingWorkflow && (
<div className={styles.loading_message}>
<p>{workflowCompleted && currentWorkflowId ? t('chat.sending_followup', 'Sending follow-up message...') : t('chat.sending_message', 'Sending message...')}</p>
</div>
)}
{startError && (
<div className={styles.error_message}>
<p>{t('chat.error_prefix', 'Error:')} {startError}</p>
</div>
)}
{messagesError && (
<div className={styles.error_message}>
<p>{t('chat.error_loading_messages', 'Error loading messages:')} {messagesError}</p>
</div>
)}
{currentWorkflowId && messagesLoading && messages.length === 0 && (
<div className={styles.loading_message}>
<p>{t('chat.loading_workflow_messages', 'Loading workflow messages...')}</p>
</div>
)}
{messages.length > 0 ? (
messages.map((message, index) => (
<MessageItem
key={message.id || index}
message={message}
index={index}
/>
))
) : !currentWorkflowId ? (
<p className={styles.placeholder_text}>{t('chat.start_conversation', 'Start a conversation by entering a message, selecting a template, or continuing a previous workflow...')}</p>
) : null}
{/* Spacer to push workflow status to bottom when there are fewer messages */}
{messages.length < 3 && <div className={styles.messages_spacer} />}
<WorkflowStatusDisplay
currentWorkflowId={currentWorkflowId}
workflowStatus={workflowStatus}
workflowCompleted={workflowCompleted}
onStartNewWorkflow={onStartNewWorkflow}
handleRetry={handleRetry}
shouldShowRetryButton={shouldShowRetryButton}
/>
<div ref={messagesEndRef} />
</div>
</motion.div>
);
};
export default MessageList;

View file

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

View file

@ -0,0 +1,76 @@
import { Prompt } from "../../../../hooks/usePrompts";
import { FileInfo } from "../../../../hooks/useFiles";
export interface DashboardChatAreaProps {
selectedPrompt?: Prompt | null;
onPromptUsed?: () => void;
onWorkflowIdChange?: (workflowId: string | null) => void;
onWorkflowCompletedChange?: (completed: boolean) => void;
resumeWorkflowId?: string | null;
}
export interface Document {
id?: string;
fileId?: number;
name: string;
url?: string;
type?: string;
size?: number;
downloadUrl?: string;
ext?: string;
}
export interface Message {
id?: string;
role: 'user' | 'assistant' | 'system';
agentName: string;
content: string;
timestamp?: string;
documents?: Document[];
}
export interface WorkflowStatus {
status: string;
currentRound?: number;
}
export interface ChatInputProps {
inputValue: string;
setInputValue: (value: string) => void;
onSend: () => void;
onKeyPress: (e: React.KeyboardEvent) => void;
isDisabled: boolean;
placeholder: string;
inputRef: React.RefObject<HTMLTextAreaElement | null>;
isWorkflowRunning: boolean;
onStopWorkflow: () => void;
isStoppingWorkflow: boolean;
attachedFiles: FileInfo[];
onFileAttach: (file: File) => void;
onFileRemove: (fileId: number) => void;
onFilesSelect: (files: FileInfo[]) => void;
}
export interface MessageListProps {
messages: Message[];
currentWorkflowId: string | null;
workflowStatus: WorkflowStatus | null;
workflowCompleted: boolean;
startingWorkflow: boolean;
startError: string | null;
messagesError: string | null;
messagesLoading: boolean;
onStartNewWorkflow: () => void;
messagesEndRef: React.RefObject<HTMLDivElement | null>;
handleRetry: () => Promise<void>;
shouldShowRetryButton: () => boolean;
}
export interface WorkflowStatusDisplayProps {
currentWorkflowId: string | null;
workflowStatus: WorkflowStatus | null;
workflowCompleted: boolean;
onStartNewWorkflow: () => void;
handleRetry: () => Promise<void>;
shouldShowRetryButton: () => boolean;
}

View file

@ -0,0 +1,2 @@
export { default } from './DashboardChatArea';
export type { DashboardChatAreaProps } from './dashboardChatAreaTypes';

View file

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

View file

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

View file

@ -0,0 +1,507 @@
import React, { useState, useRef, useEffect } from 'react';
import { useUserFiles, UserFile } from '../../../../hooks/useFiles';
import DateienAll from '../../../Dateien/DateienAll';
import DateienShared from '../../../Dateien/DateienShared';
import DateienCreated from '../../../Dateien/DateienCreated';
import DateienUploads from '../../../Dateien/DateienUploads';
interface AttachedFile {
id: number;
name: string;
size: number;
type: string;
fileData?: File;
objectUrl?: string;
}
interface FileAttachmentPopupProps {
onClose: () => void;
onFilesSelected: (files: AttachedFile[]) => void;
currentAttachedFiles: AttachedFile[];
}
const FileAttachmentPopup: React.FC<FileAttachmentPopupProps> = ({
onClose,
onFilesSelected,
currentAttachedFiles
}) => {
const [selectedFiles, setSelectedFiles] = useState<AttachedFile[]>(currentAttachedFiles);
const [dragOver, setDragOver] = useState(false);
const [uploading, setUploading] = useState(false);
const [activeTab, setActiveTab] = useState<'upload' | 'existing'>('upload');
const [fileSubTab, setFileSubTab] = useState<'all' | 'uploads' | 'created' | 'shared'>('all');
const fileInputRef = useRef<HTMLInputElement>(null);
const { files: existingFiles, loading: filesLoading } = useUserFiles();
const handleDragEnter = (e: React.DragEvent) => {
e.preventDefault();
setDragOver(true);
};
const handleDragLeave = (e: React.DragEvent) => {
e.preventDefault();
setDragOver(false);
};
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault();
};
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
setDragOver(false);
const files = Array.from(e.dataTransfer.files);
handleFileUpload(files);
};
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files ? Array.from(e.target.files) : [];
handleFileUpload(files);
};
const handleFileUpload = async (files: File[]) => {
setUploading(true);
// Create file objects with data for preview
const uploadedFiles: AttachedFile[] = files.map((file, index) => {
const fileObj: AttachedFile = {
id: Date.now() + index,
name: file.name,
size: file.size,
type: file.type,
fileData: file
};
// Create object URL for images
if (file.type.startsWith('image/')) {
fileObj.objectUrl = URL.createObjectURL(file);
console.log('FileAttachmentPopup: Created object URL for', file.name, ':', fileObj.objectUrl);
}
console.log('FileAttachmentPopup: Created file object:', fileObj.name, 'Has fileData:', !!fileObj.fileData, 'Has objectUrl:', !!fileObj.objectUrl);
return fileObj;
});
// Add to selected files
setSelectedFiles(prev => [...prev, ...uploadedFiles]);
setUploading(false);
};
const toggleFileSelection = (file: UserFile) => {
const attachedFile: AttachedFile = {
id: file.id,
name: file.file_name,
size: file.size || 0,
type: 'application/octet-stream'
};
setSelectedFiles(prev => {
const isSelected = prev.some(f => f.id === file.id);
if (isSelected) {
return prev.filter(f => f.id !== file.id);
} else {
return [...prev, attachedFile];
}
});
};
const isFileSelected = (fileId: number) => {
return selectedFiles.some(f => f.id === fileId);
};
const handleConfirm = () => {
onFilesSelected(selectedFiles);
};
// Cleanup object URLs when component unmounts
React.useEffect(() => {
return () => {
selectedFiles.forEach(file => {
if (file.objectUrl) {
URL.revokeObjectURL(file.objectUrl);
}
});
};
}, []);
const formatFileSize = (bytes: number): string => {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return Math.round(bytes / 1024) + ' KB';
return Math.round(bytes / (1024 * 1024)) + ' MB';
};
// Simplified selectable files list
const SelectableFilesList: React.FC<{
files: UserFile[];
selectedFiles: AttachedFile[];
onFileSelect: (file: UserFile) => void;
activeTab: string;
}> = ({ files, selectedFiles, onFileSelect, activeTab }) => {
// Filter files based on active tab
const filteredFiles = files.filter(file => {
switch (activeTab) {
case 'uploads':
return file.source === 'user_uploaded';
case 'created':
return file.source === 'agent_created';
case 'shared':
return file.source === 'shared_with_me';
default:
return true;
}
});
const isFileSelected = (fileId: number) => {
return selectedFiles.some(f => f.id === fileId);
};
const getFileIcon = (fileName: string) => {
const extension = fileName.split('.').pop()?.toLowerCase();
switch (extension) {
case 'pdf': return '📄';
case 'doc': case 'docx': return '📝';
case 'xls': case 'xlsx': return '📊';
case 'jpg': case 'jpeg': case 'png': case 'gif': return '🖼️';
case 'mp4': case 'avi': return '🎥';
case 'txt': return '📄';
default: return '📎';
}
};
if (filteredFiles.length === 0) {
return (
<div style={{ textAlign: 'center', padding: '40px', color: '#666' }}>
No files found in this category
</div>
);
}
return (
<div>
{/* Header */}
<div style={{
display: 'grid',
gridTemplateColumns: '40px 1fr 80px 100px 80px',
padding: '12px',
fontWeight: '500',
borderBottom: '1px solid #eee',
backgroundColor: '#f8f9fa',
fontSize: '12px'
}}>
<div></div>
<div>Name</div>
<div>Type</div>
<div>Size</div>
<div>Date</div>
</div>
{/* File List */}
<div>
{filteredFiles.map(file => (
<div
key={file.id}
onClick={() => onFileSelect(file)}
style={{
display: 'grid',
gridTemplateColumns: '40px 1fr 80px 100px 80px',
padding: '12px',
borderBottom: '1px solid #f0f0f0',
cursor: 'pointer',
backgroundColor: isFileSelected(file.id) ? '#e3f2fd' : 'white',
border: isFileSelected(file.id) ? '1px solid var(--color-secondary)' : '1px solid transparent',
transition: 'all 0.2s ease'
}}
onMouseEnter={(e) => {
if (!isFileSelected(file.id)) {
e.currentTarget.style.backgroundColor = '#f8f9fa';
}
}}
onMouseLeave={(e) => {
if (!isFileSelected(file.id)) {
e.currentTarget.style.backgroundColor = 'white';
}
}}
>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<input
type="checkbox"
checked={isFileSelected(file.id)}
onChange={() => {}}
style={{ margin: 0 }}
/>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<span style={{ fontSize: '16px' }}>{getFileIcon(file.file_name)}</span>
<span style={{ fontSize: '14px', fontWeight: '500' }}>{file.file_name}</span>
</div>
<div style={{ fontSize: '12px', color: '#666' }}>{file.action}</div>
<div style={{ fontSize: '12px', color: '#666' }}>
{file.size ? formatFileSize(file.size) : 'Unknown'}
</div>
<div style={{ fontSize: '12px', color: '#666' }}>
{new Date(file.created_at).toLocaleDateString()}
</div>
</div>
))}
</div>
</div>
);
};
return (
<div style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1000
}}>
<div style={{
backgroundColor: 'white',
borderRadius: '12px',
width: '600px',
maxHeight: '80vh',
overflow: 'hidden',
boxShadow: '0 4px 20px rgba(0, 0, 0, 0.15)'
}}>
{/* Header */}
<div style={{
padding: '20px',
borderBottom: '1px solid #eee',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center'
}}>
<h3 style={{ margin: 0 }}>Attach Files</h3>
<button
onClick={onClose}
style={{
background: 'none',
border: 'none',
fontSize: '18px',
cursor: 'pointer'
}}
>
</button>
</div>
{/* Tabs */}
<div style={{
display: 'flex',
borderBottom: '1px solid #eee'
}}>
<button
onClick={() => setActiveTab('upload')}
style={{
flex: 1,
padding: '12px',
border: 'none',
backgroundColor: activeTab === 'upload' ? '#f0f0f0' : 'white',
cursor: 'pointer',
borderBottom: activeTab === 'upload' ? '2px solid var(--color-secondary)' : 'none'
}}
>
Upload New Files
</button>
<button
onClick={() => setActiveTab('existing')}
style={{
flex: 1,
padding: '12px',
border: 'none',
backgroundColor: activeTab === 'existing' ? '#f0f0f0' : 'white',
cursor: 'pointer',
borderBottom: activeTab === 'existing' ? '2px solid var(--color-secondary)' : 'none'
}}
>
Select Existing Files
</button>
</div>
{/* Content */}
<div style={{ padding: '20px', maxHeight: '400px', overflowY: 'auto' }}>
{activeTab === 'upload' && (
<div>
{/* Drag and Drop Area */}
<div
onDragEnter={handleDragEnter}
onDragLeave={handleDragLeave}
onDragOver={handleDragOver}
onDrop={handleDrop}
style={{
border: `2px dashed ${dragOver ? 'var(--color-secondary)' : '#ccc'}`,
borderRadius: '8px',
padding: '40px',
textAlign: 'center',
backgroundColor: dragOver ? '#f0f8ff' : '#fafafa',
marginBottom: '20px',
cursor: 'pointer'
}}
onClick={() => fileInputRef.current?.click()}
>
<div style={{ fontSize: '48px', marginBottom: '16px' }}>📁</div>
<div style={{ fontSize: '16px', marginBottom: '8px' }}>
Drag and drop files here or click to browse
</div>
<div style={{ fontSize: '12px', color: '#666' }}>
Supports all file types
</div>
</div>
<input
ref={fileInputRef}
type="file"
multiple
onChange={handleFileSelect}
style={{ display: 'none' }}
/>
{uploading && (
<div style={{ textAlign: 'center', padding: '20px' }}>
Uploading files...
</div>
)}
</div>
)}
{activeTab === 'existing' && (
<div>
{/* File Category Sub-tabs */}
<div style={{
display: 'flex',
marginBottom: '16px',
borderBottom: '1px solid #eee'
}}>
{[
{ key: 'all', label: 'All Files' },
{ key: 'uploads', label: 'Uploads' },
{ key: 'created', label: 'AI Created' },
{ key: 'shared', label: 'Shared' }
].map(tab => (
<button
key={tab.key}
onClick={() => setFileSubTab(tab.key as any)}
style={{
flex: 1,
padding: '8px 12px',
border: 'none',
backgroundColor: fileSubTab === tab.key ? '#f0f0f0' : 'transparent',
cursor: 'pointer',
fontSize: '12px',
borderBottom: fileSubTab === tab.key ? '2px solid var(--color-secondary)' : '2px solid transparent'
}}
>
{tab.label}
</button>
))}
</div>
{filesLoading ? (
<div style={{ textAlign: 'center', padding: '20px' }}>
Loading files...
</div>
) : (
<div style={{
maxHeight: '300px',
overflow: 'auto',
border: '1px solid #eee',
borderRadius: '6px',
position: 'relative'
}}>
<SelectableFilesList
files={existingFiles}
selectedFiles={selectedFiles}
onFileSelect={toggleFileSelection}
activeTab={fileSubTab}
/>
</div>
)}
{/* Selection Instructions */}
<div style={{
marginTop: '12px',
padding: '8px',
backgroundColor: '#f8f9fa',
borderRadius: '4px',
fontSize: '12px',
color: '#666'
}}>
💡 Click on files to select them for attachment
</div>
</div>
)}
</div>
{/* Selected Files Summary */}
{selectedFiles.length > 0 && (
<div style={{
padding: '16px 20px',
backgroundColor: '#f8f9fa',
borderTop: '1px solid #eee'
}}>
<div style={{ fontSize: '14px', fontWeight: '500', marginBottom: '8px' }}>
Selected Files ({selectedFiles.length}):
</div>
<div style={{ maxHeight: '100px', overflowY: 'auto' }}>
{selectedFiles.map(file => (
<div key={file.id} style={{
fontSize: '12px',
color: '#666',
marginBottom: '2px'
}}>
📎 {file.name} ({formatFileSize(file.size)})
</div>
))}
</div>
</div>
)}
{/* Footer */}
<div style={{
padding: '20px',
borderTop: '1px solid #eee',
display: 'flex',
gap: '12px',
justifyContent: 'flex-end'
}}>
<button
onClick={onClose}
style={{
padding: '8px 16px',
border: '1px solid #ddd',
backgroundColor: 'white',
borderRadius: '6px',
cursor: 'pointer'
}}
>
Cancel
</button>
<button
onClick={handleConfirm}
disabled={selectedFiles.length === 0}
style={{
padding: '8px 16px',
backgroundColor: selectedFiles.length === 0 ? '#ccc' : 'var(--color-secondary)',
color: 'white',
border: 'none',
borderRadius: '6px',
cursor: selectedFiles.length === 0 ? 'not-allowed' : 'pointer'
}}
>
Attach {selectedFiles.length} File{selectedFiles.length !== 1 ? 's' : ''}
</button>
</div>
</div>
</div>
);
};
export default FileAttachmentPopup;

View file

@ -1,5 +1,4 @@
import { Prompt } from "../../../../hooks/usePrompts"; import { Prompt } from "../../../../hooks/usePrompts";
import { FileInfo } from "../../../../hooks/useFiles";
export interface DashboardChatAreaProps { export interface DashboardChatAreaProps {
selectedPrompt?: Prompt | null; selectedPrompt?: Prompt | null;
@ -9,6 +8,26 @@ export interface DashboardChatAreaProps {
resumeWorkflowId?: string | null; resumeWorkflowId?: string | null;
} }
export interface WorkflowMessage {
id: string;
role: 'user' | 'assistant' | 'system';
content: string;
timestamp?: string;
fileIds?: number[];
}
export interface FileInfo {
id: number;
name: string;
mimeType: string;
size?: number;
creationDate?: string;
downloadUrl?: string;
fileData?: File;
objectUrl?: string;
}
// Add old interfaces for compatibility
export interface Document { export interface Document {
id?: string; id?: string;
fileId?: number; fileId?: number;
@ -27,50 +46,4 @@ export interface Message {
content: string; content: string;
timestamp?: string; timestamp?: string;
documents?: Document[]; documents?: Document[];
}
export interface WorkflowStatus {
status: string;
currentRound?: number;
}
export interface ChatInputProps {
inputValue: string;
setInputValue: (value: string) => void;
onSend: () => void;
onKeyPress: (e: React.KeyboardEvent) => void;
isDisabled: boolean;
placeholder: string;
inputRef: React.RefObject<HTMLTextAreaElement | null>;
isWorkflowRunning: boolean;
onStopWorkflow: () => void;
isStoppingWorkflow: boolean;
attachedFiles: FileInfo[];
onFileAttach: (file: File) => void;
onFileRemove: (fileId: number) => void;
onFilesSelect: (files: FileInfo[]) => void;
}
export interface MessageListProps {
messages: Message[];
currentWorkflowId: string | null;
workflowStatus: WorkflowStatus | null;
workflowCompleted: boolean;
startingWorkflow: boolean;
startError: string | null;
messagesError: string | null;
messagesLoading: boolean;
onStartNewWorkflow: () => void;
messagesEndRef: React.RefObject<HTMLDivElement | null>;
handleRetry: () => Promise<void>;
shouldShowRetryButton: () => boolean;
}
export interface WorkflowStatusDisplayProps {
currentWorkflowId: string | null;
workflowStatus: WorkflowStatus | null;
workflowCompleted: boolean;
onStartNewWorkflow: () => void;
handleRetry: () => Promise<void>;
shouldShowRetryButton: () => boolean;
} }

View file

@ -3,7 +3,7 @@ import { MdOutlineRemoveRedEye } from "react-icons/md";
import styles from "./DateienItem.module.css"; import styles from "./DateienItem.module.css";
import { useState } from "react"; import { useState } from "react";
import { useFileOperations } from "../../hooks/useFiles"; import { useFileOperations } from "../../hooks/useFiles";
import FilePreviewPopup from "../Dashboard/DashboardChat/DashboardChatArea/FilePreviewPopup"; import FilePreviewPopup from "../Dashboard/DashboardChat/DashboardChatArea/DashboardChatAreaFilePreview";
import { Document } from "../Dashboard/DashboardChat/DashboardChatArea/dashboardChatAreaTypes"; import { Document } from "../Dashboard/DashboardChat/DashboardChatArea/dashboardChatAreaTypes";
import { useLanguage } from "../../contexts/LanguageContext"; import { useLanguage } from "../../contexts/LanguageContext";

View file

@ -1,32 +1,28 @@
/* Allgemeine Stile */ /* Allgemeine Stile */
.sidebarContainer { .sidebarContainer {
border-radius: 30px; border-radius: 0px;
border: none; border-right: 1px solid var(--color-primary);
background: var(--color-bg); background: var(--color-bg);
box-shadow: 0px 2px 6px 0px rgba(194, 194, 194, 0.10); box-shadow: 0px 2px 6px 0px rgba(194, 194, 194, 0.10);
width: 240px; width: 240px;
margin-top: 51px;
margin-left: 49px;
padding-bottom: 1px; padding-bottom: 1px;
display: flex; display: flex;
justify-content: center; justify-content: top;
align-items: center; align-items: center;
flex-direction: column; flex-direction: column;
height: 100vh;
} }
.sidebar { .sidebar {
display: flex; display: flex;
width: 240px;
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
flex-shrink: 0; flex-shrink: 0;
margin: 0 0 30px 0;
font-family: var(--font-family); font-family: var(--font-family);
} }
.logoContainer { .logoContainer {
display: flex; display: flex;
width: 100%;
height: 80px; /* Fixed height instead of auto */ height: 80px; /* Fixed height instead of auto */
padding: 30px 20px 7px 20px; padding: 30px 20px 7px 20px;
justify-content: space-between; justify-content: space-between;
@ -38,7 +34,7 @@
.logoWrapper { .logoWrapper {
display: flex; display: flex;
justify-content: center; justify-content: left;
align-items: center; align-items: center;
flex: 1; flex: 1;
} }
@ -49,24 +45,42 @@
color: var(--color-primary); color: var(--color-primary);
} }
/* Toggle Button Styles */ .logoText {
.toggleButton { font-family: var(--font-family);
background: var(--color-primary); font-size: 35px;
border: none;
border-radius: 10px;
color: white;
width: 30px;
height: 30px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; letter-spacing: -0.5px;
font-weight: 200;
}
.logoPower {
color: var(--color-text);
}
.logoOn {
color: var(--color-secondary);
font-weight: 700;
}
/* Toggle Button Styles */
.toggleButton {
background: none;
border: none;
border-radius: 10px;
color: var(--color-text);
width: 50px;
height: 50px;
display: flex;
align-items: center;
justify-content: right;
cursor: pointer; cursor: pointer;
transition: all 0.3s ease; transition: all 0.3s ease;
flex-shrink: 0; flex-shrink: 0;
} }
.toggleButton:hover { .toggleButton:hover {
background: var(--color-primary-hover); background:none;
transform: scale(1.05); transform: scale(1.05);
} }
@ -74,7 +88,7 @@
.sidebarContainer.minimized { .sidebarContainer.minimized {
width: 80px; width: 80px;
display: flex; display: flex;
justify-content: center; justify-content: top;
align-items: center; align-items: center;
} }
@ -93,7 +107,9 @@
} }
.sidebarContainer.minimized .toggleButton { .sidebarContainer.minimized .toggleButton {
margin: 0 auto; /* Center the toggle button */ margin: 0 auto;
justify-content: center;
/* Center the toggle button */
} }

View file

@ -46,7 +46,10 @@ const Sidebar: React.FC<SidebarProps> = ({ data }) => {
exit={{ opacity: 0, scale: 0.8 }} exit={{ opacity: 0, scale: 0.8 }}
transition={{ duration: 0.2 }} transition={{ duration: 0.2 }}
> >
<img src="/logos/PowerOn_transparent.png" alt="Logo" className={styles.logo} /> <div className={styles.logoText}>
<span className={styles.logoPower}>Power</span>
<span className={styles.logoOn}>On</span>
</div>
</motion.div> </motion.div>
)} )}
</AnimatePresence> </AnimatePresence>

View file

@ -7,7 +7,7 @@
.menu li { .menu li {
display: flex; display: flex;
width: 200px; width: 220px;
height: 44px; height: 44px;
padding: 0 3px 0 15px; padding: 0 3px 0 15px;
align-items: center; align-items: center;
@ -55,7 +55,6 @@
padding: 2.292px 2.3px 2.508px 2.292px; padding: 2.292px 2.3px 2.508px 2.292px;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
margin-left: 20px;
flex-shrink: 0; flex-shrink: 0;
} }

View file

@ -1,14 +1,12 @@
.user_section { .user_section {
display: flex; display: flex;
width: 240px; width: 240px;
height: 100px; /* Fixed height instead of auto */
padding: 20px; padding: 20px;
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: left;
gap: 8px; gap: 8px;
font-family: var(--font-family); font-family: var(--font-family);
box-sizing: border-box; /* Include padding in height calculation */ box-sizing: border-box;
margin-bottom: 30px;
} }
.user_info { .user_info {
@ -37,9 +35,10 @@
.user_section h1 { .user_section h1 {
margin: 0; margin: 0;
font-size: 16pt; font-size: 16pt;
line-height: 1.2; line-height: 1.;
color: var(--color-text); color: var(--color-text);
font-family: var(--font-family); font-family: var(--font-family);
font-weight: 400;
transition: opacity 0.3s ease; transition: opacity 0.3s ease;
white-space: nowrap; white-space: nowrap;
} }
@ -85,7 +84,6 @@
/* Minimized User Section Styles */ /* Minimized User Section Styles */
.user_section.minimized { .user_section.minimized {
width: 46px; /* Match menu item width */ width: 46px; /* Match menu item width */
height: 100px; /* Same fixed height as expanded */
padding: 20px 15px 20px 15px; /* Match menu item padding structure */ padding: 20px 15px 20px 15px; /* Match menu item padding structure */
align-items: center; align-items: center;
box-sizing: border-box; box-sizing: border-box;

View file

@ -45,18 +45,11 @@ const SidebarUser: React.FC<SidebarUserProps> = ({ user, isLoading, error, isMin
return ( return (
<div className={`${styles.user_section} ${isMinimized ? styles.minimized : ''}`}> <div className={`${styles.user_section} ${isMinimized ? styles.minimized : ''}`}>
<div className={styles.user_info}> <div className={styles.user_info}>
<FaUserCircle className={styles.user_icon} />
<div className={styles.text_content}> <div className={styles.text_content}>
<h1>{ user.name }</h1> <h1>{ user.name }</h1>
<p>Rolle: {user.role}</p> <p>Rolle: {user.role}</p>
</div> </div>
</div> </div>
<button
className={styles.logout_button}
onClick={handleLogout}
>
<span className={styles.logout_text}>Logout</span>
</button>
</div> </div>
) )
} }

View file

@ -1,63 +1,24 @@
.dashboardContainer { .dashboardContainer {
margin: 51px 49px 0 36px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 20px; gap: 20px;
font-family: var(--font-family); font-family: var(--font-family);
width: 98%; width: 100%;
max-height: calc(100vh - 100px); height: 100vh;
flex: 1;
} }
.chatLogContainer { .chatLogContainer {
display: flex; display: flex;
gap: 20px;
transition: all 0.3s ease;
}
.chatLogContainer.expanded {
flex-direction: column; flex-direction: column;
gap: 20px; flex: 1;
min-height: 0;
} }
/* Height classes for different states */ .chatArea {
.chatArea15vh { display: flex;
height: 35vh; flex-direction: column;
flex: 1;
min-height: 0;
} }
.chatArea40vh {
height: 60vh;
}
.chatArea45vh {
height: 60vh;
}
.chatArea60vh {
height: 85vh;
}
.logArea15vh {
height: 10vh;
}
.logArea25vh {
height: 25vh;
}
.logArea40vh {
height: 60vh;
}
.logArea60vh {
height: 85vh;
}
.promptArea30vh {
height: 30vh;
}
.promptArea40vh {
height: 40vh;
}

View file

@ -42,58 +42,12 @@ function Dashboard () {
}, []); }, []);
// Determine CSS classes based on states // Determine CSS classes based on states
const getPromptClass = () => {
if (isPromptAreaCollapsed) return '';
return isChatExpanded ? styles.promptArea40vh : styles.promptArea30vh;
};
const getChatClass = () => {
if (isPromptAreaCollapsed && isChatExpanded) return styles.chatArea45vh;
if (!isPromptAreaCollapsed && isChatExpanded) return styles.chatArea15vh;
if (isPromptAreaCollapsed && !isChatExpanded) return styles.chatArea60vh;
return styles.chatArea40vh;
};
const getLogClass = () => {
if (isPromptAreaCollapsed && isChatExpanded) return styles.logArea25vh;
if (!isPromptAreaCollapsed && isChatExpanded) return styles.logArea15vh;
if (isPromptAreaCollapsed && !isChatExpanded) return styles.logArea60vh;
return styles.logArea40vh;
};
// Memoize style objects to prevent infinite re-renders
const promptStyle = useMemo(() => ({
marginBottom: !isPromptAreaCollapsed ? "0px" : "0"
}), [isPromptAreaCollapsed]);
const chatStyle = useMemo(() => ({
width: isChatExpanded ? "100%" : "calc(50% - 10px)",
flex: isChatExpanded ? "none" : "1",
marginBottom: isChatExpanded ? "0px" : "0"
}), [isChatExpanded]);
const logStyle = useMemo(() => ({
width: isChatExpanded ? "100%" : "calc(50% - 10px)",
flex: isChatExpanded ? "none" : "1"
}), [isChatExpanded]);
return ( return (
<div className={styles.dashboardContainer}> <div className={styles.dashboardContainer}>
<div
className={getPromptClass()}
style={promptStyle}
>
<DashboardPrompt
onPromptRun={handlePromptRun}
isCollapsed={isPromptAreaCollapsed}
onToggleCollapse={() => setIsPromptAreaCollapsed(!isPromptAreaCollapsed)}
/>
</div>
<div className={`${styles.chatLogContainer} ${isChatExpanded ? styles.expanded : ''}`}> <div className={`${styles.chatLogContainer} ${isChatExpanded ? styles.expanded : ''}`}>
<div <div className={styles.chatArea}>
className={getChatClass()}
style={chatStyle}
>
<DashboardChat <DashboardChat
isExpanded={isChatExpanded} isExpanded={isChatExpanded}
onToggleExpand={handleChatToggleExpand} onToggleExpand={handleChatToggleExpand}
@ -104,16 +58,6 @@ function Dashboard () {
onWorkflowResume={handleWorkflowResume} onWorkflowResume={handleWorkflowResume}
/> />
</div> </div>
<div
className={getLogClass()}
style={logStyle}
>
<DashboardLog
isExpanded={isChatExpanded}
workflowId={currentWorkflowId}
workflowCompleted={workflowCompleted}
/>
</div>
</div> </div>
</div> </div>
); );

View file

@ -59,14 +59,16 @@
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 20px; padding: 20px;
background: var(--color-surface); background: var(--color-bg);
border-radius: 20px; border-radius: 20px;
border: 2px solid var(--color-surface);
gap: 20px; gap: 20px;
} }
.settingInfo { .settingInfo {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
color: var(--color-text);
gap: 5px; gap: 5px;
flex: 1; flex: 1;
} }
@ -80,7 +82,7 @@
.settingDescription { .settingDescription {
font-size: 0.875rem; font-size: 0.875rem;
color: var(--color-gray); color: var(--color-primary);
font-family: var(--font-family); font-family: var(--font-family);
} }
@ -90,7 +92,7 @@
gap: 12px; gap: 12px;
padding: 12px 20px; padding: 12px 20px;
border-radius: 25px; border-radius: 25px;
border: 2px solid var(--color-gray-disabled); border: 2px solid var(--color-primary);
background: var(--color-bg); background: var(--color-bg);
color: var(--color-text); color: var(--color-text);
cursor: pointer; cursor: pointer;
@ -107,19 +109,6 @@
box-shadow: 0 4px 12px rgba(63, 81, 181, 0.15); box-shadow: 0 4px 12px rgba(63, 81, 181, 0.15);
} }
.themeToggle.light {
background: linear-gradient(135deg, var(--color-bg) 0%, var(--color-surface) 100%);
}
.themeToggle.dark {
background: linear-gradient(135deg, var(--color-surface) 0%, var(--color-bg) 100%);
border-color: var(--color-primary);
}
.themeToggle.dark:hover {
border-color: var(--color-primary-hover);
box-shadow: 0 4px 12px rgba(178, 102, 255, 0.15);
}
.toggleSlider { .toggleSlider {
display: flex; display: flex;

View file

@ -7,7 +7,6 @@
font-family: var(--font-family); font-family: var(--font-family);
z-index: 0; z-index: 0;
overflow: hidden; overflow: hidden;
padding: 0 49px 0 0;
} }
.homeContainer::before { .homeContainer::before {
@ -22,16 +21,21 @@
pointer-events: none; pointer-events: none;
} }
.homeSidebar {
height: auto;
}
.homeContent {
height: 100vh;
width: 100%;
}
.body { .body {
display: flex; display: flex;
max-width: 100vw; max-width: 100vw;
height: 100vh; height: 100vh;
}
.homeSidebar {
flex-shrink: 0;
}
.homeContent {
height: 100vh;
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
} }

View file

@ -25,6 +25,7 @@ function Home () {
initial={{ opacity: 0 }} initial={{ opacity: 0 }}
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
transition={{ duration: 0.3, ease: "easeInOut" }} transition={{ duration: 0.3, ease: "easeInOut" }}
style={{ height: "100%", display: "flex", flexDirection: "column" }}
> >
<Outlet /> <Outlet />
</motion.div> </motion.div>