added file attachement for messages and fixed preview tool

This commit is contained in:
idittrich-valueon 2025-05-30 12:21:10 +02:00
parent 057abf4a88
commit 56c33869ac
28 changed files with 4162 additions and 345 deletions

1104
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -25,6 +25,7 @@
"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"
},
"devDependencies": {

View file

@ -2,8 +2,8 @@
import axios from 'axios';
const api = axios.create({
baseURL: 'https://gateway.poweron-center.net',
//baseURL: 'http://localhost:8000',
//baseURL: 'https://gateway.poweron-center.net',
baseURL: 'http://localhost:8000',
withCredentials: true
});

View file

@ -83,9 +83,9 @@
.horizontalLine {
width: 100%;
background-color: black;
height: 2px;
margin-top: 19px;
background-color: var(--Grayscale-Black, #24262B);
height: 1px;
margin-top: 20px;
}
.horizontalLineLight {

View file

@ -38,14 +38,28 @@
min-height: 100%;
display: flex;
flex-direction: column;
justify-content: flex-end;
justify-content: flex-start;
padding-bottom: 10px;
}
.messages_spacer {
flex: 1;
min-height: 20px;
}
.chat_input {
display: flex;
gap: 10px;
align-items: center;
align-items: flex-end;
flex-shrink: 0;
flex-direction: column;
}
.input_row {
display: flex;
gap: 10px;
align-items: center;
width: 100%;
}
.message_input {
@ -67,10 +81,10 @@
opacity: 0.6;
}
.send_button {
padding: 12px 12px;
background-color: var(--Brand-Green-Green, #3A8088);
color: white;
.attachment_button {
padding: 11px 11px;
background-color: #e6f2f2;
color: var(--Brand-Green-Green, #3A8088);
border: none;
border-radius: 12px;
cursor: pointer;
@ -79,6 +93,88 @@
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.2s ease, border-color 0.2s ease;
}
.attachment_button:hover {
background-color: #cce3e4;
color: var(--Brand-Green-Green, #3A8088);
}
.attachment_button:disabled {
background-color: #f5f5f5;
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: #f0f8f8;
border: 1px solid #cce7e8;
border-radius: 8px;
font-size: 12px;
color: #3a8088;
}
.attached_file_icon {
font-size: 14px;
}
.attached_file_name {
max-width: 150px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.attached_file_remove {
background: none;
border: none;
color: #999;
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: #ddd;
color: #666;
}
.send_button {
padding: 12px 12px;
background-color: var(--Brand-Green-Green, #3A8088);
color: white;
border: 1px solid var(--Brand-Green-Green, #3A8088);
border-radius: 12px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
display: flex;
align-items: center;
justify-content: center;
}
.send_button:hover {
background-color: #34737b;
}
.send_button_icon {
@ -92,6 +188,7 @@
background-color: #ccc;
cursor: not-allowed;
opacity: 0.6;
border: 1px solid #ccc;
}
.stop_button {
@ -135,15 +232,15 @@
.error_message {
padding: 10px;
background-color: #ffebee;
border-left: 4px solid #f44336;
background-color: #fceff0;
border-left: 4px solid #d85d67;
border-radius: 4px;
margin-bottom: 10px;
}
.error_message p {
margin: 0;
color: #c62828;
color: #d85d67;
font-size: 14px;
}
@ -205,31 +302,31 @@
.workflow_status {
padding: 8px 12px;
background-color: #e8f5e8;
border-left: 4px solid #4caf50;
background-color: #e6f2f2;
border-left: 4px solid #3a8088;
border-radius: 4px;
margin-bottom: 10px;
margin: 10px 0;
}
.workflow_status p {
margin: 0;
color: #2e7d32;
color: #3a8088;
font-size: 13px;
font-style: italic;
}
.completion_message {
padding: 10px 12px;
background-color: #e8f5e8;
border-left: 4px solid #4caf50;
background-color: #e6f2f2;
border-left: 4px solid #3a8088;
border-radius: 4px;
margin-bottom: 10px;
margin: 10px 0;
text-align: center;
}
.completion_message p {
margin: 0 0 10px 0;
color: #2e7d32;
color: #3a8088;
font-size: 14px;
font-weight: 600;
}
@ -238,7 +335,7 @@
background-color: var(--Brand-Green-Green, #3A8088);
color: white;
border: none;
border-radius: 8px;
border-radius: 15px;
padding: 8px 16px;
font-size: 12px;
font-weight: 500;
@ -249,3 +346,108 @@
.new_workflow_button:hover {
background-color: #2d6b73;
}
.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: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 8px;
font-size: 13px;
transition: background-color 0.2s ease;
cursor: pointer;
text-decoration: none;
color: inherit;
}
.document_item:hover {
background-color: rgba(255, 255, 255, 0.2);
}
.message_assistant .document_item {
background-color: rgba(0, 0, 0, 0.05);
border-color: rgba(0, 0, 0, 0.1);
}
.message_assistant .document_item:hover {
background-color: rgba(0, 0, 0, 0.1);
}
.document_icon {
width: 16px;
height: 16px;
flex-shrink: 0;
opacity: 0.8;
}
.document_info {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}
.document_name {
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.document_meta {
font-size: 11px;
opacity: 0.7;
display: flex;
gap: 8px;
}
.document_size {
white-space: nowrap;
}
.document_type {
white-space: nowrap;
}
.document_actions {
display: flex;
gap: 4px;
margin-left: auto;
flex-shrink: 0;
}
.document_action_button {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border: none;
border-radius: 6px;
background-color: rgba(255, 255, 255, 0.15);
color: inherit;
cursor: pointer;
transition: background-color 0.2s ease;
font-size: 14px;
}
.document_action_button:hover {
background-color: rgba(255, 255, 255, 0.25);
}
.message_assistant .document_action_button {
background-color: rgba(0, 0, 0, 0.08);
}
.message_assistant .document_action_button:hover {
background-color: rgba(0, 0, 0, 0.15);
}

View file

@ -18,6 +18,7 @@ const DashboardChatArea: React.FC<DashboardChatAreaProps> = ({
setInputValue,
currentWorkflowId,
workflowCompleted,
attachedFiles,
// Refs
inputRef,
@ -36,6 +37,9 @@ const DashboardChatArea: React.FC<DashboardChatAreaProps> = ({
handleKeyPress,
startNewWorkflow,
handleStopWorkflow,
handleFileAttach,
handleFileRemove,
handleFilesSelect,
// Workflow state
isWorkflowRunning,
@ -81,6 +85,10 @@ const DashboardChatArea: React.FC<DashboardChatAreaProps> = ({
isWorkflowRunning={isWorkflowRunning}
onStopWorkflow={handleStopWorkflow}
isStoppingWorkflow={isStoppingWorkflow}
attachedFiles={attachedFiles}
onFileAttach={handleFileAttach}
onFileRemove={handleFileRemove}
onFilesSelect={handleFilesSelect}
/>
</div>
);

View file

@ -1,10 +1,33 @@
import React from "react";
import React, { useState } 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 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,
@ -15,8 +38,27 @@ const ChatInput: React.FC<ChatInputProps> = ({
inputRef,
isWorkflowRunning,
onStopWorkflow,
isStoppingWorkflow
isStoppingWorkflow,
attachedFiles,
onFileAttach,
onFileRemove,
onFilesSelect
}) => {
const [isUploadModalOpen, setIsUploadModalOpen] = useState(false);
const handleAttachmentClick = () => {
setIsUploadModalOpen(true);
};
const handleFilesSelected = (files: FileInfo[]) => {
onFilesSelect(files);
setIsUploadModalOpen(false);
};
const handleFileRemove = (fileId: number) => {
onFileRemove(fileId);
};
return (
<motion.div
className={styles.chat_input}
@ -24,29 +66,76 @@ const ChatInput: React.FC<ChatInputProps> = ({
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3, duration: 0.3, ease: "easeOut" }}
>
<input
ref={inputRef}
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyPress={onKeyPress}
placeholder={placeholder}
className={styles.message_input}
disabled={isDisabled}
{/* 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="Datei entfernen"
>
<IoClose size={12} />
</button>
</div>
))}
</div>
)}
{/* Input row with text input, attachment button, and send button */}
<div className={styles.input_row}>
<input
ref={inputRef}
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyPress={onKeyPress}
placeholder={placeholder}
className={styles.message_input}
disabled={isDisabled}
/>
{/* Attachment button */}
<motion.button
className={styles.attachment_button}
onClick={handleAttachmentClick}
disabled={isDisabled || isWorkflowRunning}
whileTap={{ scale: 0.95 }}
transition={{ duration: 0.2, ease: "easeOut" }}
title="Datei anhängen"
>
<IoAttach size={18} />
</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.button
className={isWorkflowRunning ? styles.stop_button : styles.send_button}
onClick={isWorkflowRunning ? onStopWorkflow : onSend}
disabled={isWorkflowRunning ? isStoppingWorkflow : (isDisabled || !inputValue.trim())}
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>
</motion.div>
);
};

View file

@ -1,5 +1,8 @@
import React from "react";
import { Message } from "./dashboardChatAreaTypes";
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 styles from './DashboardChatArea.module.css';
interface MessageItemProps {
@ -7,7 +10,100 @@ interface MessageItemProps {
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 [previewDocument, setPreviewDocument] = useState<Document | null>(null);
const [isPreviewOpen, setIsPreviewOpen] = useState(false);
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();
console.log('handlePreview called with document:', document);
console.log('document.id:', document.id, 'document.fileId:', document.fileId);
// Use fileId if available, otherwise try to use id as fallback
const fileId = document.fileId || document.id;
if (!fileId) {
console.error('Neither fileId nor id is available on document:', document);
return;
}
console.log('Using fileId for preview:', fileId, 'type:', typeof fileId);
setPreviewDocument(document);
setIsPreviewOpen(true);
};
const handleClosePreview = () => {
setIsPreviewOpen(false);
setPreviewDocument(null);
};
const handleDownload = (document: Document, e: React.MouseEvent) => {
e.stopPropagation();
// TODO: Implement download functionality
console.log('Download document:', document.name);
};
return (
<div
key={message.id || index}
@ -20,11 +116,66 @@ const MessageItem: React.FC<MessageItemProps> = ({ message, index }) => {
<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={`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="Preview document"
>
<MdOutlineRemoveRedEye />
</button>
<button
className={styles.document_action_button}
onClick={(e) => handleDownload(document, e)}
title="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>
);
};

View file

@ -54,9 +54,12 @@ const MessageList: React.FC<MessageListProps> = ({
/>
))
) : !currentWorkflowId ? (
<p className={styles.placeholder_text}>Start a conversation by typing a message...</p>
<p className={styles.placeholder_text}>Start a conversation by typing a message, selecting a prompt 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}

View file

@ -0,0 +1,452 @@
.overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.7);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
padding: 20px;
}
.popup {
background: white;
border-radius: 12px;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
max-width: 90vw;
height: 90vh;
width: 800px;
display: flex;
flex-direction: column;
overflow: hidden;
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
border-bottom: 1px solid #e5e7eb;
background-color: #f9fafb;
flex-shrink: 0;
}
.title {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #111827;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1;
margin-right: 16px;
}
.close_button {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border: none;
border-radius: 8px;
background-color: transparent;
color: #6b7280;
cursor: pointer;
transition: all 0.2s ease;
font-size: 18px;
flex-shrink: 0;
}
.close_button:hover {
background-color: #e5e7eb;
color: #374151;
}
.content {
flex: 1;
display: flex;
align-items: flex-start;
justify-content: center;
padding: 20px;
overflow: hidden;
height: 0;
}
.loading {
display: flex;
align-items: center;
justify-content: center;
color: #6b7280;
font-size: 16px;
width: 100%;
height: 100%;
}
.error {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #dc2626;
font-size: 16px;
text-align: center;
width: 100%;
height: 100%;
}
.no_preview {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #6b7280;
font-size: 16px;
font-style: italic;
width: 100%;
height: 100%;
}
.image_preview {
max-width: 100%;
max-height: 100%;
object-fit: contain;
border-radius: 8px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
.pdf_preview {
width: 100%;
height: 100%;
border: none;
border-radius: 8px;
}
.text_preview {
width: 100%;
height: 100%;
background-color: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 16px;
font-family: 'Courier New', monospace;
font-size: 14px;
line-height: 1.5;
color: #334155;
overflow: auto;
white-space: pre-wrap;
word-wrap: break-word;
box-sizing: border-box;
}
.enhanced_text_preview {
width: 100%;
height: 100%;
background-color: #ffffff;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 32px;
overflow: auto;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
box-sizing: border-box;
line-height: 1.7;
color: #374151;
}
.text_line {
margin-bottom: 4px;
word-wrap: break-word;
white-space: pre-wrap;
}
.text_line_break {
height: 16px;
}
.text_header {
font-weight: 600;
font-size: 1.1em;
color: #1f2937;
margin: 16px 0 8px 0;
padding-bottom: 4px;
border-bottom: 1px solid #e5e7eb;
}
.text_numbered {
margin: 8px 0;
padding-left: 8px;
color: #4b5563;
}
.text_bullet {
margin: 4px 0;
padding-left: 8px;
color: #4b5563;
}
.text_indented {
background-color: #f8fafc;
padding: 8px 12px;
margin: 4px 0;
border-left: 3px solid #d1d5db;
border-radius: 4px;
font-family: 'Courier New', monospace;
font-size: 0.9em;
color: #6b7280;
}
.code_preview {
width: 100%;
height: 100%;
background-color: #1f2937;
border: 1px solid #374151;
border-radius: 8px;
padding: 16px;
font-family: 'Courier New', monospace;
font-size: 14px;
line-height: 1.5;
color: #f9fafb;
overflow: auto;
white-space: pre-wrap;
word-wrap: break-word;
box-sizing: border-box;
}
.python_code_preview {
width: 100%;
height: 100%;
background-color: #0d1117;
border: 1px solid #30363d;
border-radius: 8px;
overflow: hidden;
box-sizing: border-box;
display: flex;
flex-direction: column;
}
.code_header {
background-color: #161b22;
padding: 12px 16px;
border-bottom: 1px solid #30363d;
display: flex;
justify-content: space-between;
align-items: center;
flex-shrink: 0;
}
.code_language {
background-color: #1f6feb;
color: #ffffff;
padding: 4px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.code_filename {
color: #7d8590;
font-size: 14px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
font-weight: 500;
}
.python_code_content {
flex: 1;
margin: 0;
padding: 16px;
background-color: #0d1117;
color: #e6edf3;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 14px;
line-height: 1.6;
overflow: auto;
white-space: pre;
tab-size: 4;
}
.python_code_content code {
font-family: inherit;
font-size: inherit;
color: inherit;
background: none;
padding: 0;
}
.markdown_preview {
width: 100%;
height: 100%;
background-color: #ffffff;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 24px;
overflow: auto;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
box-sizing: border-box;
}
.markdown_preview h1 {
font-size: 2em;
font-weight: 600;
margin: 0 0 16px 0;
padding-bottom: 8px;
border-bottom: 1px solid #e2e8f0;
color: #1f2937;
}
.markdown_preview h2 {
font-size: 1.5em;
font-weight: 600;
margin: 24px 0 12px 0;
color: #374151;
}
.markdown_preview h3 {
font-size: 1.25em;
font-weight: 600;
margin: 20px 0 8px 0;
color: #4b5563;
}
.markdown_preview h4,
.markdown_preview h5,
.markdown_preview h6 {
font-size: 1em;
font-weight: 600;
margin: 16px 0 8px 0;
color: #6b7280;
}
.markdown_preview p {
margin: 0 0 16px 0;
line-height: 1.7;
color: #374151;
}
.markdown_preview ul,
.markdown_preview ol {
margin: 0 0 16px 20px;
padding-left: 20px;
}
.markdown_preview li {
margin: 4px 0;
line-height: 1.6;
color: #374151;
}
.markdown_preview blockquote {
margin: 16px 0;
padding: 12px 16px;
background-color: #f8fafc;
border-left: 4px solid #3b82f6;
color: #4b5563;
font-style: italic;
}
.markdown_preview code {
background-color: #f1f5f9;
padding: 2px 4px;
border-radius: 4px;
font-family: 'Courier New', monospace;
font-size: 0.875em;
color: #dc2626;
}
.markdown_preview pre {
background-color: #1f2937;
color: #f9fafb;
padding: 16px;
border-radius: 8px;
overflow-x: auto;
margin: 16px 0;
font-family: 'Courier New', monospace;
font-size: 0.875em;
line-height: 1.5;
}
.markdown_preview pre code {
background: none;
padding: 0;
color: inherit;
border-radius: 0;
}
.markdown_preview table {
width: 100%;
border-collapse: collapse;
margin: 16px 0;
border: 1px solid #e2e8f0;
}
.markdown_preview th,
.markdown_preview td {
padding: 8px 12px;
text-align: left;
border: 1px solid #e2e8f0;
}
.markdown_preview th {
background-color: #f8fafc;
font-weight: 600;
color: #374151;
}
.markdown_preview td {
color: #4b5563;
}
.markdown_preview a {
color: #3b82f6;
text-decoration: underline;
}
.markdown_preview a:hover {
color: #1d4ed8;
}
.markdown_preview hr {
border: none;
height: 1px;
background-color: #e2e8f0;
margin: 24px 0;
}
.markdown_preview strong {
font-weight: 600;
color: #1f2937;
}
.markdown_preview em {
font-style: italic;
color: #4b5563;
}
/* Responsive design */
@media (max-width: 768px) {
.popup {
width: 95vw;
max-height: 95vh;
}
.header {
padding: 12px 16px;
}
.title {
font-size: 16px;
}
.content {
padding: 16px;
}
.pdf_preview {
height: 400px;
}
}

View file

@ -0,0 +1,232 @@
import React, { useEffect } from "react";
import ReactMarkdown from 'react-markdown';
import { MdClose } from "react-icons/md";
import { Document } from "./dashboardChatAreaTypes";
import { useFilePreview } from "../../../../hooks/useWorkflows";
import styles from './FilePreviewPopup.module.css';
interface FilePreviewPopupProps {
document: Document;
isOpen: boolean;
onClose: () => void;
}
const FilePreviewPopup: React.FC<FilePreviewPopupProps> = ({ document, isOpen, onClose }) => {
const { previewContent, fileMetadata, isLoading, error, fetchPreview, clearPreview } = useFilePreview();
useEffect(() => {
if (isOpen && document) {
// Use fileId if available, otherwise try to use id as fallback
const fileId = document.fileId || document.id;
if (fileId) {
console.log('FilePreviewPopup: calling fetchPreview with fileId:', fileId);
fetchPreview(String(fileId));
} else {
console.error('FilePreviewPopup: No fileId or id available on document:', document);
}
} else if (!isOpen) {
clearPreview();
}
}, [isOpen, document.fileId, document.id]);
const handleBackdropClick = (e: React.MouseEvent) => {
if (e.target === e.currentTarget) {
onClose();
}
};
const getPreviewComponent = () => {
if (isLoading) {
return <div className={styles.loading}>Loading preview...</div>;
}
if (error) {
return (
<div className={styles.error}>
<div>Error: {error}</div>
{fileMetadata && (
<div style={{ marginTop: '10px', fontSize: '12px', opacity: 0.7 }}>
Debug: {JSON.stringify(fileMetadata, null, 2)}
</div>
)}
</div>
);
}
if (!previewContent) {
return (
<div className={styles.no_preview}>
<div>No preview available</div>
{fileMetadata && (
<div style={{ marginTop: '10px', fontSize: '12px', opacity: 0.7 }}>
Available metadata: {Object.keys(fileMetadata).join(', ')}
<br />
Preview field: {fileMetadata.preview ? 'has data' : 'empty/null'}
<br />
Base64Encoded: {String(fileMetadata.base64Encoded)}
<br />
MimeType: {fileMetadata.mimeType}
</div>
)}
</div>
);
}
// Use metadata from backend response
const mimeType = fileMetadata?.mimeType;
const isBase64Encoded = fileMetadata?.base64Encoded;
const fileExtension = document.ext?.toLowerCase();
// Check if this is a markdown file by extension/MIME type first
const isMarkdownByType = fileExtension === 'md' ||
fileExtension === 'markdown' ||
mimeType === 'text/markdown' ||
mimeType === 'text/x-markdown';
// Content-based markdown detection for .txt files with markdown content
// BUT NOT for specific code file types
const isCodeFile = fileExtension === 'py' ||
fileExtension === 'js' ||
fileExtension === 'ts' ||
fileExtension === 'jsx' ||
fileExtension === 'tsx' ||
fileExtension === 'java' ||
fileExtension === 'cpp' ||
fileExtension === 'c' ||
fileExtension === 'php' ||
fileExtension === 'html' ||
fileExtension === 'css';
const hasMarkdownContent = !isCodeFile && previewContent && (
previewContent.includes('# ') || // Headers
previewContent.includes('## ') || // Headers
previewContent.includes('**') || // Bold
previewContent.includes('__') || // Bold
previewContent.includes('- ') || // Lists
previewContent.includes('* ') || // Lists
previewContent.includes('1. ') || // Numbered lists
previewContent.includes('```') || // Code blocks
previewContent.includes('[') && previewContent.includes('](') // Links
);
const isMarkdown = isMarkdownByType || (mimeType === 'text/plain' && hasMarkdownContent);
if (mimeType?.startsWith('image/')) {
// Image preview
const imageSrc = isBase64Encoded
? `data:${mimeType};base64,${previewContent}`
: previewContent;
return (
<img
src={imageSrc}
alt={document.name}
className={styles.image_preview}
/>
);
} else if (fileExtension === 'pdf' || mimeType === 'application/pdf') {
// PDF preview
const pdfSrc = isBase64Encoded
? `data:application/pdf;base64,${previewContent}`
: previewContent;
return (
<iframe
src={pdfSrc}
className={styles.pdf_preview}
title={document.name}
/>
);
} else if (isMarkdown) {
// Markdown preview
console.log('Rendering markdown with ReactMarkdown:', previewContent?.substring(0, 200));
return (
<div className={styles.markdown_preview}>
<ReactMarkdown>{previewContent}</ReactMarkdown>
</div>
);
} else if (fileExtension === 'py') {
// Python code preview
return (
<div className={styles.python_code_preview}>
<div className={styles.code_header}>
<span className={styles.code_language}>Python</span>
<span className={styles.code_filename}>
{document.ext ? `${document.name}.${document.ext}` : document.name}
</span>
</div>
<pre className={styles.python_code_content}>
<code>{previewContent}</code>
</pre>
</div>
);
} else if (mimeType?.startsWith('text/') || fileExtension === 'txt') {
// Enhanced text preview for all text files
return (
<div className={styles.enhanced_text_preview}>
{previewContent?.split('\n').map((line, index) => {
// Handle empty lines
if (line.trim() === '') {
return <div key={index} className={styles.text_line_break}></div>;
}
// Check if line looks like a header (all caps, starts with numbers, etc.)
const isHeader = line.match(/^[A-Z\s\d\.\-_]+:?\s*$/) && line.length < 80;
const isNumberedItem = line.match(/^\s*\d+\.\s/);
const isBulletItem = line.match(/^\s*[-*•]\s/);
const isIndented = line.match(/^\s{4,}/);
return (
<div
key={index}
className={`${styles.text_line} ${
isHeader ? styles.text_header :
isNumberedItem ? styles.text_numbered :
isBulletItem ? styles.text_bullet :
isIndented ? styles.text_indented : ''
}`}
>
{line}
</div>
);
})}
</div>
);
} else {
// Code/raw text preview for non-text files
return (
<pre className={styles.code_preview}>
{previewContent}
</pre>
);
}
};
if (!isOpen) return null;
return (
<div className={styles.overlay} onClick={handleBackdropClick}>
<div className={styles.popup}>
<div className={styles.header}>
<h3 className={styles.title}>
{document.ext ? `${document.name}.${document.ext}` : document.name}
</h3>
<button
className={styles.close_button}
onClick={onClose}
title="Close preview"
>
<MdClose />
</button>
</div>
<div className={styles.content}>
{getPreviewComponent()}
</div>
</div>
</div>
);
};
export default FilePreviewPopup;

View file

@ -1,5 +1,6 @@
import { useState, useEffect, useRef } from "react";
import { Prompt } from "../../../../hooks/usePrompts";
import { FileInfo, useFileOperations } from "../../../../hooks/useFiles";
import { useWorkflowOperations, useWorkflowMessages, useWorkflowStatus } from "../../../../hooks/useWorkflows";
interface UseChatLogicProps {
@ -18,12 +19,14 @@ export const useChatLogic = ({
const [inputValue, setInputValue] = useState("");
const [currentWorkflowId, setCurrentWorkflowId] = useState<string | null>(null);
const [workflowCompleted, setWorkflowCompleted] = useState(false);
const [attachedFiles, setAttachedFiles] = useState<FileInfo[]>([]);
const inputRef = useRef<HTMLInputElement>(null);
const messagesEndRef = useRef<HTMLDivElement>(null);
const { startWorkflow, startingWorkflow, startError, stopWorkflow, stoppingWorkflows } = useWorkflowOperations();
const { messages, loading: messagesLoading, error: messagesError, refetch: refetchMessages } = useWorkflowMessages(currentWorkflowId);
const { status: workflowStatus, refetch: refetchStatus } = useWorkflowStatus(currentWorkflowId);
const { handleFileUpload } = useFileOperations();
// Update input value when a prompt is selected
useEffect(() => {
@ -36,12 +39,12 @@ export const useChatLogic = ({
}
}, [selectedPrompt]);
// Auto-scroll to bottom when new messages arrive
// Auto-scroll to bottom when new messages arrive or workflow status changes
useEffect(() => {
if (messagesEndRef.current) {
messagesEndRef.current.scrollIntoView({ behavior: 'smooth' });
}
}, [messages]);
}, [messages, workflowCompleted, workflowStatus]);
// Polling logic for fetching messages and status
useEffect(() => {
@ -81,22 +84,65 @@ export const useChatLogic = ({
setCurrentWorkflowId(resumeWorkflowId);
setWorkflowCompleted(false);
setInputValue("");
setAttachedFiles([]);
}
}, [resumeWorkflowId, currentWorkflowId]);
const handleFileAttach = async (file: File) => {
try {
console.log('Uploading file:', file.name);
const result = await handleFileUpload(file, currentWorkflowId || undefined);
if (result.success && result.fileData) {
// Add the uploaded file to the attached files list
const fileInfo: FileInfo = {
id: result.fileData.id,
name: result.fileData.name || file.name,
mimeType: result.fileData.mimeType || file.type,
size: result.fileData.size || file.size,
creationDate: result.fileData.creationDate || new Date().toISOString(),
workflowId: currentWorkflowId || undefined
};
setAttachedFiles(prev => [...prev, fileInfo]);
console.log('File uploaded successfully:', fileInfo);
} else {
console.error('Failed to upload file:', result.error);
}
} catch (error) {
console.error('Error uploading file:', error);
}
};
const handleFileRemove = (fileId: number) => {
setAttachedFiles(prev => prev.filter(file => file.id !== fileId));
};
const handleFilesSelect = (selectedFiles: FileInfo[]) => {
// Add selected files to the attached files list, avoiding duplicates
setAttachedFiles(prev => {
const existingIds = new Set(prev.map(f => f.id));
const newFiles = selectedFiles.filter(f => !existingIds.has(f.id));
return [...prev, ...newFiles];
});
};
const handleSend = async () => {
if (inputValue.trim()) {
console.log('Sending message:', inputValue);
if (inputValue.trim() || attachedFiles.length > 0) {
console.log('Sending message:', inputValue, 'with files:', attachedFiles.map(f => f.id));
try {
let result;
// Prepare file IDs for the request
const listFileId = attachedFiles.map(file => file.id);
// If we have a completed workflow, send as follow-up using the existing workflow ID
if (workflowCompleted && currentWorkflowId) {
console.log('Sending follow-up message to workflow:', currentWorkflowId);
result = await startWorkflow({
prompt: inputValue,
listFileId: []
prompt: inputValue || "Files attached", // Provide a default message if only files are sent
listFileId: listFileId
}, currentWorkflowId);
if (result.success) {
@ -113,8 +159,8 @@ export const useChatLogic = ({
setWorkflowCompleted(false);
result = await startWorkflow({
prompt: inputValue,
listFileId: []
prompt: inputValue || "Files attached", // Provide a default message if only files are sent
listFileId: listFileId
});
if (result.success && result.data) {
@ -126,8 +172,9 @@ export const useChatLogic = ({
}
if (result.success) {
// Clear the input after successful send
// Clear the input and attached files after successful send
setInputValue("");
setAttachedFiles([]);
// Call onPromptUsed if a prompt was used
if (selectedPrompt && onPromptUsed) {
onPromptUsed();
@ -152,6 +199,7 @@ export const useChatLogic = ({
setCurrentWorkflowId(null);
setWorkflowCompleted(false);
setInputValue("");
setAttachedFiles([]);
if (onWorkflowIdChange) {
onWorkflowIdChange(null);
}
@ -187,6 +235,7 @@ export const useChatLogic = ({
setInputValue,
currentWorkflowId,
workflowCompleted,
attachedFiles,
// Refs
inputRef,
@ -205,6 +254,9 @@ export const useChatLogic = ({
handleKeyPress,
startNewWorkflow,
handleStopWorkflow,
handleFileAttach,
handleFileRemove,
handleFilesSelect,
// Workflow state
isWorkflowRunning,

View file

@ -1,4 +1,5 @@
import { Prompt } from "../../../../hooks/usePrompts";
import { FileInfo } from "../../../../hooks/useFiles";
export interface DashboardChatAreaProps {
selectedPrompt?: Prompt | null;
@ -8,11 +9,23 @@ export interface DashboardChatAreaProps {
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';
content: string;
timestamp?: string;
documents?: Document[];
}
export interface WorkflowStatus {
@ -31,6 +44,10 @@ export interface ChatInputProps {
isWorkflowRunning: boolean;
onStopWorkflow: () => void;
isStoppingWorkflow: boolean;
attachedFiles: FileInfo[];
onFileAttach: (file: File) => void;
onFileRemove: (fileId: number) => void;
onFilesSelect: (files: FileInfo[]) => void;
}
export interface MessageListProps {

View file

@ -0,0 +1,142 @@
import React, { useState } from 'react';
import { FaSort, FaSortUp, FaSortDown } from "react-icons/fa";
import { motion, AnimatePresence } from "framer-motion";
import DateienItem from './DateienItem';
import { UserFile } from '../../hooks/useFiles';
import styles from './DateienLists.module.css';
// Sort types
type SortField = 'file_name' | 'action' | 'size' | 'created_at' | 'source';
type SortDirection = 'asc' | 'desc';
interface DateienAllProps {
files: UserFile[];
onFileDeleted: () => void;
onOptimisticDelete?: (fileId: number) => void;
}
const DateienAll: React.FC<DateienAllProps> = ({ files, onFileDeleted, onOptimisticDelete }) => {
const [sortField, setSortField] = useState<SortField>('created_at');
const [sortDirection, setSortDirection] = useState<SortDirection>('desc');
// Handle sorting
const handleSort = (field: SortField) => {
if (field === sortField) {
setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc');
} else {
setSortField(field);
setSortDirection(field === 'created_at' ? 'desc' : 'asc');
}
};
// Sort all files
const sortedFiles = [...files].sort((a, b) => {
let result = 0;
switch (sortField) {
case 'file_name':
result = a.file_name.localeCompare(b.file_name);
break;
case 'action':
result = a.action.localeCompare(b.action);
break;
case 'size':
const sizeA = a.size ?? 0;
const sizeB = b.size ?? 0;
result = sizeA - sizeB;
break;
case 'created_at':
result = new Date(a.created_at).getTime() - new Date(b.created_at).getTime();
break;
case 'source':
result = a.source?.localeCompare(b.source ?? '') ?? 0;
break;
}
return sortDirection === 'asc' ? result : -result;
});
// Helper to render sort icon
const renderSortIcon = (field: SortField) => {
if (sortField !== field) return <FaSort className={styles.sortIcon} />;
return sortDirection === 'asc' ?
<FaSortUp className={styles.sortIcon} /> :
<FaSortDown className={styles.sortIcon} />;
};
if (sortedFiles.length === 0) {
return (
<motion.div
className={styles.noFilesMessage}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.3 }}
>
<p>Keine Dateien gefunden.</p>
</motion.div>
);
}
return (
<motion.div
className={styles.filesTable}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.3 }}
>
{/* Table Headers */}
<div className={styles.tableHeader}>
<div className={styles.headerCell} onClick={() => handleSort('file_name')}>
<span>Name</span>
{renderSortIcon('file_name')}
</div>
<div className={styles.headerCell} onClick={() => handleSort('action')}>
<span>Typ</span>
{renderSortIcon('action')}
</div>
<div className={styles.headerCell} onClick={() => handleSort('size')}>
<span>Größe</span>
{renderSortIcon('size')}
</div>
<div className={styles.headerCell} onClick={() => handleSort('created_at')}>
<span>Datum</span>
{renderSortIcon('created_at')}
</div>
</div>
{/* Files List */}
<motion.ul
className={styles.filesList}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.4, delay: 0.1 }}
>
<AnimatePresence mode="popLayout">
{sortedFiles.map((file: UserFile) => (
<motion.div
key={file.id}
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{
opacity: 0,
scale: 0.95,
x: -50,
transition: { duration: 0.2, ease: "easeIn" }
}}
transition={{ duration: 0.2 }}
layout
>
<DateienItem
file={file}
onDelete={onFileDeleted}
onOptimisticDelete={onOptimisticDelete ? () => onOptimisticDelete(file.id) : undefined}
/>
</motion.div>
))}
</AnimatePresence>
</motion.ul>
</motion.div>
);
};
export default DateienAll;

View file

@ -0,0 +1,137 @@
import React, { useState } from 'react';
import { FaSort, FaSortUp, FaSortDown } from "react-icons/fa";
import { motion, AnimatePresence } from "framer-motion";
import DateienItem from './DateienItem';
import { UserFile } from '../../hooks/useFiles';
import styles from './DateienLists.module.css';
// Sort types
type SortField = 'file_name' | 'action' | 'size' | 'created_at';
type SortDirection = 'asc' | 'desc';
interface DateienCreatedProps {
files: UserFile[];
onFileDeleted: () => void;
onOptimisticDelete?: (fileId: number) => void;
}
const DateienCreated: React.FC<DateienCreatedProps> = ({ files, onFileDeleted, onOptimisticDelete }) => {
const [sortField, setSortField] = useState<SortField>('created_at');
const [sortDirection, setSortDirection] = useState<SortDirection>('desc');
// Filter files for created (agent_created)
const createdFiles = files.filter(file => file.source === 'agent_created');
// Handle sorting
const handleSort = (field: SortField) => {
if (field === sortField) {
setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc');
} else {
setSortField(field);
setSortDirection(field === 'created_at' ? 'desc' : 'asc');
}
};
// Sort files
const sortedFiles = [...createdFiles].sort((a, b) => {
let result = 0;
switch (sortField) {
case 'file_name':
result = a.file_name.localeCompare(b.file_name);
break;
case 'action':
result = a.action.localeCompare(b.action);
break;
case 'size':
const sizeA = a.size ?? 0;
const sizeB = b.size ?? 0;
result = sizeA - sizeB;
break;
case 'created_at':
result = new Date(a.created_at).getTime() - new Date(b.created_at).getTime();
break;
}
return sortDirection === 'asc' ? result : -result;
});
// Helper to render sort icon
const renderSortIcon = (field: SortField) => {
if (sortField !== field) return <FaSort className={styles.sortIcon} />;
return sortDirection === 'asc' ?
<FaSortUp className={styles.sortIcon} /> :
<FaSortDown className={styles.sortIcon} />;
};
if (sortedFiles.length === 0) {
return (
<motion.div
className={styles.noFilesMessage}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.3 }}
>
<p>Keine von der KI erstellten Dateien gefunden.</p>
</motion.div>
);
}
return (
<motion.div
className={styles.filesTable}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.3 }}
>
{/* Table Headers */}
<div className={styles.tableHeader}>
<div className={styles.headerCell} onClick={() => handleSort('file_name')}>
<span>Name</span>
{renderSortIcon('file_name')}
</div>
<div className={styles.headerCell} onClick={() => handleSort('action')}>
<span>Typ</span>
{renderSortIcon('action')}
</div>
<div className={styles.headerCell} onClick={() => handleSort('size')}>
<span>Größe</span>
{renderSortIcon('size')}
</div>
<div className={styles.headerCell} onClick={() => handleSort('created_at')}>
<span>Datum</span>
{renderSortIcon('created_at')}
</div>
</div>
{/* Files List with AI-created indicator */}
<motion.ul
className={`${styles.filesList} ${styles.aiCreated}`}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.4, delay: 0.1 }}
>
<AnimatePresence mode="popLayout">
{sortedFiles.map((file: UserFile) => (
<motion.div
key={file.id}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
layout
>
<DateienItem
file={file}
onDelete={onFileDeleted}
onOptimisticDelete={onOptimisticDelete ? () => onOptimisticDelete(file.id) : undefined}
/>
</motion.div>
))}
</AnimatePresence>
</motion.ul>
</motion.div>
);
};
export default DateienCreated;

View file

@ -0,0 +1,342 @@
.overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.7);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
padding: 20px;
}
.modal {
background: white;
border-radius: 12px;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
max-width: 90vw;
max-height: 90vh;
width: 900px;
height: 700px;
display: flex;
flex-direction: column;
overflow: hidden;
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20px;
border-bottom: 1px solid #e5e7eb;
background-color: #f9fafb;
flex-shrink: 0;
}
.header h2 {
margin: 0;
font-size: 20px;
font-weight: 600;
color: #111827;
}
.closeButton {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border: none;
border-radius: 8px;
background-color: transparent;
color: #6b7280;
cursor: pointer;
transition: all 0.2s ease;
font-size: 18px;
}
.closeButton:hover {
background-color: #e5e7eb;
color: #374151;
}
.content {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.tabNavigation {
display: flex;
border-bottom: 1px solid #e5e7eb;
background-color: #f9fafb;
padding: 0 20px;
}
.tabButton {
padding: 12px 16px;
border: none;
background: none;
color: #6b7280;
font-size: 14px;
font-weight: 500;
cursor: pointer;
border-bottom: 2px solid transparent;
transition: all 0.2s ease;
white-space: nowrap;
}
.tabButton:hover {
color: #374151;
}
.tabButton.active {
color: var(--Brand-Green-Green, #3A8088);
border-bottom-color: var(--Brand-Green-Green, #3A8088);
}
.actionBar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
border-bottom: 1px solid #e5e7eb;
background-color: #ffffff;
}
.selectionControls {
display: flex;
align-items: center;
gap: 16px;
}
.selectAllButton {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border: 1px solid #d1d5db;
border-radius: 6px;
background-color: #ffffff;
color: #374151;
font-size: 14px;
cursor: pointer;
transition: all 0.2s ease;
}
.selectAllButton:hover {
background-color: #f9fafb;
border-color: #9ca3af;
}
.selectionCount {
font-size: 14px;
color: #6b7280;
font-weight: 500;
}
.uploadButton {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 16px;
border: none;
border-radius: 8px;
background-color: var(--Brand-Green-Green, #3A8088);
color: white;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s ease;
}
.uploadButton:hover {
background-color: #2d6b73;
}
.fileListContainer {
flex: 1;
overflow-y: auto;
padding: 20px;
}
.loading,
.error,
.noFiles {
display: flex;
align-items: center;
justify-content: center;
height: 200px;
color: #6b7280;
font-size: 16px;
text-align: center;
}
.error {
color: #dc2626;
}
.selectableFileList {
display: flex;
flex-direction: column;
gap: 8px;
}
.selectableFileItem {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
border: 1px solid #e5e7eb;
border-radius: 8px;
background-color: #ffffff;
cursor: pointer;
transition: all 0.2s ease;
}
.selectableFileItem:hover {
background-color: #f9fafb;
border-color: #d1d5db;
}
.selectableFileItem.selected {
background-color: #f0f8f8;
border-color: var(--Brand-Green-Green, #3A8088);
}
.fileCheckbox {
display: flex;
align-items: center;
font-size: 18px;
color: #9ca3af;
}
.checkedIcon {
color: var(--Brand-Green-Green, #3A8088);
}
.uncheckedIcon {
color: #d1d5db;
}
.fileIcon {
display: flex;
align-items: center;
font-size: 16px;
color: #6b7280;
}
.fileInfo {
flex: 1;
min-width: 0;
}
.fileName {
font-weight: 500;
color: #111827;
margin-bottom: 2px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.fileDetails {
font-size: 12px;
color: #6b7280;
}
.footer {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 12px;
padding: 20px;
border-top: 1px solid #e5e7eb;
background-color: #f9fafb;
flex-shrink: 0;
}
.cancelButton {
padding: 10px 16px;
border: 1px solid #d1d5db;
border-radius: 8px;
background-color: #ffffff;
color: #374151;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.cancelButton:hover {
background-color: #f9fafb;
border-color: #9ca3af;
}
.confirmButton {
padding: 10px 16px;
border: none;
border-radius: 8px;
background-color: var(--Brand-Green-Green, #3A8088);
color: white;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s ease;
}
.confirmButton:hover:not(:disabled) {
background-color: #2d6b73;
}
.confirmButton:disabled {
background-color: #9ca3af;
cursor: not-allowed;
}
/* Responsive design */
@media (max-width: 768px) {
.modal {
width: 95vw;
height: 85vh;
}
.header h2 {
font-size: 18px;
}
.tabNavigation {
padding: 0 12px;
overflow-x: auto;
}
.tabButton {
padding: 10px 12px;
font-size: 13px;
}
.actionBar {
flex-direction: column;
gap: 12px;
align-items: stretch;
}
.selectionControls {
justify-content: space-between;
}
.fileListContainer {
padding: 12px;
}
.selectableFileItem {
padding: 10px;
}
.footer {
padding: 16px;
}
}

View file

@ -0,0 +1,310 @@
import React, { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { IoClose, IoCheckbox, IoSquareOutline, IoCloudUploadOutline } from 'react-icons/io5';
import { FaFile } from 'react-icons/fa';
import { useUserFiles, UserFile, FileInfo } from '../../../hooks/useFiles';
import DateienUploadTool from './DateienUploadTool';
import DateienAll from '../DateienAll';
import DateienUploads from '../DateienUploads';
import DateienCreated from '../DateienCreated';
import DateienShared from '../DateienShared';
import styles from './DateienSelector.module.css';
type FileListType = 'all' | 'uploads' | 'created' | 'shared';
interface DateienSelectorProps {
isOpen: boolean;
onClose: () => void;
onFilesSelected: (files: FileInfo[]) => void;
allowMultiple?: boolean;
}
const DateienSelector: React.FC<DateienSelectorProps> = ({
isOpen,
onClose,
onFilesSelected,
allowMultiple = true
}) => {
const [selectedFiles, setSelectedFiles] = useState<Set<number>>(new Set());
const [activeTab, setActiveTab] = useState<FileListType>('all');
const [showUploadTool, setShowUploadTool] = useState(false);
const { files, loading, error, refetch } = useUserFiles();
// Filter files based on source
const getFilteredFiles = (files: UserFile[], type: FileListType): UserFile[] => {
switch (type) {
case 'uploads':
return files.filter(file => file.source === 'user_uploaded');
case 'created':
return files.filter(file => file.source === 'agent_created');
case 'shared':
return files.filter(file => file.source === 'shared_with_me');
case 'all':
default:
return files;
}
};
const filteredFiles = getFilteredFiles(files, activeTab);
// Reset selection when tab changes
useEffect(() => {
setSelectedFiles(new Set());
}, [activeTab]);
// Reset state when modal closes
useEffect(() => {
if (!isOpen) {
setSelectedFiles(new Set());
setShowUploadTool(false);
}
}, [isOpen]);
const handleFileSelect = (fileId: number) => {
if (allowMultiple) {
setSelectedFiles(prev => {
const newSet = new Set(prev);
if (newSet.has(fileId)) {
newSet.delete(fileId);
} else {
newSet.add(fileId);
}
return newSet;
});
} else {
setSelectedFiles(new Set([fileId]));
}
};
const handleSelectAll = () => {
if (selectedFiles.size === filteredFiles.length) {
setSelectedFiles(new Set());
} else {
setSelectedFiles(new Set(filteredFiles.map(file => file.id)));
}
};
const handleConfirmSelection = () => {
const selectedFileObjects: FileInfo[] = files
.filter(file => selectedFiles.has(file.id))
.map(file => ({
id: file.id,
name: file.file_name,
mimeType: deriveMimeTypeFromAction(file.action),
size: file.size,
creationDate: file.created_at,
source: file.source
}));
onFilesSelected(selectedFileObjects);
onClose();
};
// Helper function to derive MIME type from action (reverse mapping)
const deriveMimeTypeFromAction = (action: string): string => {
switch (action.toLowerCase()) {
case 'bild':
return 'image/jpeg'; // Default image type
case 'pdf':
return 'application/pdf';
case 'dokument':
return 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
case 'tabelle':
return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
case 'text':
return 'text/plain';
case 'video':
return 'video/mp4'; // Default video type
case 'audio':
return 'audio/mpeg'; // Default audio type
default:
return 'application/octet-stream'; // Default binary type
}
};
const handleFileUpload = (file: File) => {
// Refresh file list after upload
refetch();
setShowUploadTool(false);
};
const renderFileListComponent = () => {
const commonProps = {
files: filteredFiles,
onFileDeleted: refetch
};
switch (activeTab) {
case 'uploads':
return <DateienUploads {...commonProps} />;
case 'created':
return <DateienCreated {...commonProps} />;
case 'shared':
return <DateienShared {...commonProps} />;
case 'all':
default:
return <DateienAll {...commonProps} />;
}
};
const getTabLabel = (type: FileListType) => {
const counts = {
all: files.length,
uploads: files.filter(f => f.source === 'user_uploaded').length,
created: files.filter(f => f.source === 'agent_created').length,
shared: files.filter(f => f.source === 'shared_with_me').length
};
const labels = {
all: 'Alle Dateien',
uploads: 'Hochgeladen',
created: 'KI-erstellt',
shared: 'Geteilt'
};
return `${labels[type]} (${counts[type]})`;
};
if (!isOpen) return null;
if (showUploadTool) {
return (
<DateienUploadTool
isOpen={true}
onClose={() => setShowUploadTool(false)}
onFileUpload={handleFileUpload}
/>
);
}
return (
<div className={styles.overlay}>
<motion.div
className={styles.modal}
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.9 }}
transition={{ duration: 0.3 }}
>
<div className={styles.header}>
<h2>Dateien auswählen</h2>
<button className={styles.closeButton} onClick={onClose}>
<IoClose />
</button>
</div>
<div className={styles.content}>
{/* Tab Navigation */}
<div className={styles.tabNavigation}>
{(['all', 'uploads', 'created', 'shared'] as FileListType[]).map(tab => (
<button
key={tab}
className={`${styles.tabButton} ${activeTab === tab ? styles.active : ''}`}
onClick={() => setActiveTab(tab)}
>
{getTabLabel(tab)}
</button>
))}
</div>
{/* Action Bar */}
<div className={styles.actionBar}>
<div className={styles.selectionControls}>
{allowMultiple && filteredFiles.length > 0 && (
<button
className={styles.selectAllButton}
onClick={handleSelectAll}
>
{selectedFiles.size === filteredFiles.length ? (
<>
<IoCheckbox /> Alle abwählen
</>
) : (
<>
<IoSquareOutline /> Alle auswählen
</>
)}
</button>
)}
{selectedFiles.size > 0 && (
<span className={styles.selectionCount}>
{selectedFiles.size} Datei{selectedFiles.size !== 1 ? 'en' : ''} ausgewählt
</span>
)}
</div>
<button
className={styles.uploadButton}
onClick={() => setShowUploadTool(true)}
>
<IoCloudUploadOutline />
Neue Datei hochladen
</button>
</div>
{/* File List */}
<div className={styles.fileListContainer}>
{loading ? (
<div className={styles.loading}>Dateien werden geladen...</div>
) : error ? (
<div className={styles.error}>Fehler beim Laden der Dateien: {error}</div>
) : filteredFiles.length === 0 ? (
<div className={styles.noFiles}>Keine Dateien in dieser Kategorie gefunden.</div>
) : (
<div className={styles.selectableFileList}>
{filteredFiles.map(file => (
<div
key={file.id}
className={`${styles.selectableFileItem} ${
selectedFiles.has(file.id) ? styles.selected : ''
}`}
onClick={() => handleFileSelect(file.id)}
>
<div className={styles.fileCheckbox}>
{selectedFiles.has(file.id) ? (
<IoCheckbox className={styles.checkedIcon} />
) : (
<IoSquareOutline className={styles.uncheckedIcon} />
)}
</div>
<div className={styles.fileIcon}>
<FaFile />
</div>
<div className={styles.fileInfo}>
<div className={styles.fileName}>{file.file_name}</div>
<div className={styles.fileDetails}>
{file.action} {file.size ? `${Math.round(file.size / 1024)} KB` : 'Unbekannte Größe'}
</div>
</div>
</div>
))}
</div>
)}
</div>
</div>
{/* Footer */}
<div className={styles.footer}>
<button
className={styles.cancelButton}
onClick={onClose}
>
Abbrechen
</button>
<button
className={styles.confirmButton}
onClick={handleConfirmSelection}
disabled={selectedFiles.size === 0}
>
{selectedFiles.size === 0
? 'Dateien auswählen'
: `${selectedFiles.size} Datei${selectedFiles.size !== 1 ? 'en' : ''} hinzufügen`
}
</button>
</div>
</motion.div>
</div>
);
};
export default DateienSelector;

View file

@ -11,25 +11,46 @@
z-index: 1000;
}
.h2 {
font-size: 24px;
font-weight: 600;
font-family: 'Arial', sans-serif;
}
.modal {
background: white;
padding: 2rem;
border-radius: 8px;
padding: 35px 40px 30px 40px;
border-radius: 30px;
width: 90%;
max-width: 500px;
position: relative;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.modalHeader {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.modalHeader h2 {
margin: 0;
font-size: 1.5rem;
color: #333;
}
.closeButton {
position: absolute;
top: 1rem;
right: 1rem;
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: #666;
padding: 0.25rem;
display: flex;
align-items: center;
justify-content: center;
}
.closeButton:hover {
@ -43,27 +64,27 @@
.uploadStatus {
padding: 1rem;
border-radius: 4px;
border-radius: 15px;
margin-bottom: 1rem;
text-align: center;
font-weight: 500;
}
.uploadStatus.success {
background-color: rgba(76, 175, 80, 0.1);
color: #388e3c;
border: 1px solid #4caf50;
background-color: #e6f2f2;
color: #3a8088;
border: 1px solid #3a8088;
}
.uploadStatus.error {
background-color: rgba(244, 67, 54, 0.1);
color: #d32f2f;
border: 1px solid #f44336;
background-color: #fceff0;
color: #d85d67;
border: 1px solid #d85d67;
}
.dropzone {
border: 2px dashed #ccc;
border-radius: 4px;
border-radius: 15px;
padding: 2rem;
text-align: center;
cursor: pointer;
@ -72,13 +93,13 @@
}
.dropzone.active {
border-color: #2196f3;
background-color: rgba(33, 150, 243, 0.1);
border-color: #3a8088;
background-color: #e6f2f2;
}
.dropzone.uploading {
border-color: #4caf50;
background-color: rgba(76, 175, 80, 0.1);
border-color: #3a8088;
background-color: #e6f2f2;
cursor: wait;
}
@ -97,46 +118,43 @@
}
.browseButton {
background-color: #2196f3;
background-color: #3a8088;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 4px;
border-radius: 15px;
cursor: pointer;
margin-top: 0.5rem;
}
.browseButton:hover {
background-color: #1976d2;
background-color: #34737b;
}
.browseButton:disabled {
background-color: #90caf9;
background-color: #f4f3f5;
color: #888098;
cursor: not-allowed;
}
.selectedFile {
margin-top: 1rem;
padding: 1rem;
background-color: #f5f5f5;
border-radius: 4px;
}
.uploadButton {
background-color: #4caf50;
background-color: #3a8088;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 4px;
border-radius: 15px;
cursor: pointer;
margin-top: 0.5rem;
}
.uploadButton:hover {
background-color: #388e3c;
background-color: #34737b;
}
.uploadButton:disabled {
background-color: #a5d6a7;
background-color: #f4f3f5;
color: #888098;
cursor: not-allowed;
}

View file

@ -1,17 +1,17 @@
import React, { useCallback, useState } from 'react';
import { useDropzone } from 'react-dropzone';
import styles from './DateienUpload.module.css';
import styles from './DateienUploadTool.module.css';
import { IoCloudUploadOutline } from "react-icons/io5";
import { IoClose } from "react-icons/io5";
import { useFileOperations } from '../../../hooks/useFiles';
interface DateienUploadProps {
interface DateienUploadToolProps {
isOpen: boolean;
onClose: () => void;
onFileUpload: (file: File) => void;
}
function DateienUpload({ isOpen, onClose, onFileUpload }: DateienUploadProps) {
function DateienUploadTool({ isOpen, onClose, onFileUpload }: DateienUploadToolProps) {
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const [isUploading, setIsUploading] = useState(false);
const [uploadStatus, setUploadStatus] = useState<{ success: boolean; message: string } | null>(null);
@ -71,10 +71,12 @@ function DateienUpload({ isOpen, onClose, onFileUpload }: DateienUploadProps) {
return (
<div className={styles.overlay}>
<div className={styles.modal}>
<button className={styles.closeButton} onClick={onClose} disabled={isUploading}>
<IoClose />
</button>
<h2>Datei hochladen</h2>
<div className={styles.modalHeader}>
<h2>Datei hochladen</h2>
<button className={styles.closeButton} onClick={onClose} disabled={isUploading}>
<IoClose />
</button>
</div>
{uploadStatus && (
<div className={`${styles.uploadStatus} ${uploadStatus.success ? styles.success : styles.error}`}>
@ -120,4 +122,4 @@ function DateienUpload({ isOpen, onClose, onFileUpload }: DateienUploadProps) {
);
}
export default DateienUpload;
export default DateienUploadTool;

View file

@ -14,13 +14,12 @@
/* Column layout matching the header structure */
.fileName {
flex: 3;
display: flex;
align-items: center;
overflow: hidden;
font-weight: 500;
color: #333;
padding-right: 8px;
padding-left: 14px; /* Align with table header */
}
.fileName span {
@ -31,16 +30,24 @@
}
.fileType {
flex: 1;
font-size: 14px;
color: #666;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
display: flex;
flex-direction: column;
gap: 2px;
}
.fileSource {
font-size: 12px;
color: #888;
font-weight: 400;
opacity: 0.8;
}
.fileSize {
flex: 1;
font-size: 14px;
color: #666;
overflow: hidden;
@ -56,12 +63,12 @@
}
.fileDate {
flex: 1.5;
font-size: 14px;
color: #666;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
margin-right: 8px;
}
.icon {
@ -71,14 +78,15 @@
}
.actionButtons {
flex: 1;
display: flex;
gap: 4px;
justify-content: flex-end;
margin-left: auto;
}
.downloadButton,
.deleteButton {
.deleteButton,
.previewButton {
display: flex;
align-items: center;
justify-content: center;
@ -94,7 +102,8 @@
}
.downloadButton:hover:not(:disabled),
.deleteButton:hover:not(:disabled) {
.deleteButton:hover:not(:disabled),
.previewButton:hover:not(:disabled) {
background-color: var(--background-color-hover, #e8e8e8);
color: var(--text-color-primary, #333);
}
@ -103,6 +112,10 @@
color: #dc2626;
}
.previewButton:hover:not(:disabled) {
color: #3b82f6;
}
.deleteButton.confirm {
background-color: #fee2e2;
color: #dc2626;
@ -113,7 +126,8 @@
}
.downloadButton:disabled,
.deleteButton:disabled {
.deleteButton:disabled,
.previewButton:disabled {
cursor: not-allowed;
opacity: 0.7;
}

View file

@ -1,7 +1,10 @@
import { FaFile, FaDownload, FaTrash } from "react-icons/fa";
import { MdOutlineRemoveRedEye } from "react-icons/md";
import styles from "./DateienItem.module.css";
import { useState } from "react";
import { useFileOperations } from "../../hooks/useFiles";
import FilePreviewPopup from "../Dashboard/DashboardChat/DashboardChatArea/FilePreviewPopup";
import { Document } from "../Dashboard/DashboardChat/DashboardChatArea/dashboardChatAreaTypes";
type DateienItemProps = {
file: {
@ -10,8 +13,10 @@ type DateienItemProps = {
action: string;
created_at: string;
size?: number;
source?: string; // 'user_uploaded', 'agent_created', or 'shared_with_me'
};
onDelete?: () => void;
onOptimisticDelete?: () => void; // New prop for immediate UI update
};
/**
@ -30,9 +35,27 @@ const formatFileSize = (bytes?: number): string => {
return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${sizes[i]}`;
};
const DateienItem = ({ file, onDelete }: DateienItemProps) => {
/**
* Formats the file source for display
*/
const formatFileSource = (source?: string): string => {
switch (source) {
case 'user_uploaded':
return 'Hochgeladen';
case 'agent_created':
return 'KI-erstellt';
case 'shared_with_me':
return 'Geteilt';
default:
return 'Unbekannt';
}
};
const DateienItem = ({ file, onDelete, onOptimisticDelete }: DateienItemProps) => {
const { downloadingFiles, deletingFiles, handleFileDownload, handleFileDelete } = useFileOperations();
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [previewDocument, setPreviewDocument] = useState<Document | null>(null);
const [isPreviewOpen, setIsPreviewOpen] = useState(false);
const isDownloading = downloadingFiles.has(file.id);
const isDeleting = deletingFiles.has(file.id);
@ -58,9 +81,12 @@ const DateienItem = ({ file, onDelete }: DateienItemProps) => {
const handleDeleteClick = async () => {
if (showDeleteConfirm) {
const success = await handleFileDelete(file.id);
if (success && onDelete) {
onDelete();
const success = await handleFileDelete(file.id, onOptimisticDelete);
if (!success) {
// If deletion failed, refresh the file list to restore the file
if (onDelete) {
onDelete();
}
}
setShowDeleteConfirm(false);
} else {
@ -72,54 +98,98 @@ const DateienItem = ({ file, onDelete }: DateienItemProps) => {
setShowDeleteConfirm(false);
};
const handlePreview = () => {
// Split filename to get name and extension
const nameParts = file.file_name.split('.');
const extension = nameParts.length > 1 ? nameParts.pop() : undefined;
const fileName = nameParts.join('.');
// Create a Document object compatible with FilePreviewPopup
const document: Document = {
id: String(file.id),
fileId: file.id,
name: fileName,
ext: extension,
size: file.size
};
setPreviewDocument(document);
setIsPreviewOpen(true);
};
const handleClosePreview = () => {
setIsPreviewOpen(false);
setPreviewDocument(null);
};
return (
<li>
{/* 1st column: Name with icon */}
<div className={styles.fileName}>
<FaFile className={styles.icon} />
<span>{file.file_name}</span>
</div>
{/* 2nd column: Type */}
<div className={styles.fileType}>
{file.action}
</div>
{/* 3rd column: Size */}
<div className={styles.fileSize}>
{formatFileSize(file.size)}
</div>
{/* 4th column: Date and action buttons */}
<div className={styles.fileDateWithActions}>
<span className={styles.fileDate}>
{formatDate(file.created_at)}
</span>
<div className={styles.actionButtons}>
<button
className={`${styles.downloadButton} ${isDownloading ? styles.downloading : ''}`}
onClick={() => handleFileDownload(file.id, file.file_name)}
disabled={isDownloading || isDeleting}
title="Download file"
>
<FaDownload className={styles.actionIcon} />
{isDownloading && <span className={styles.actionText}>Downloading...</span>}
</button>
<button
className={`${styles.deleteButton} ${isDeleting ? styles.deleting : ''} ${showDeleteConfirm ? styles.confirm : ''}`}
onClick={handleDeleteClick}
disabled={isDownloading || isDeleting}
title={showDeleteConfirm ? "Click again to confirm deletion" : "Delete file"}
onBlur={handleCancelDelete}
>
<FaTrash className={styles.actionIcon} />
{isDeleting && <span className={styles.actionText}>Deleting...</span>}
{showDeleteConfirm && <span className={styles.actionText}>Click to confirm</span>}
</button>
<>
<li>
{/* 1st column: Name with icon */}
<div className={styles.fileName}>
<FaFile className={styles.icon} />
<span>{file.file_name}</span>
</div>
</div>
</li>
{/* 2nd column: Type with source */}
<div className={styles.fileType}>
<div>{file.action}</div>
<div className={styles.fileSource}>{formatFileSource(file.source)}</div>
</div>
{/* 3rd column: Size */}
<div className={styles.fileSize}>
{formatFileSize(file.size)}
</div>
{/* 4th column: Date and action buttons */}
<div className={styles.fileDateWithActions}>
<span className={styles.fileDate}>
{formatDate(file.created_at)}
</span>
<div className={styles.actionButtons}>
<button
className={styles.previewButton}
onClick={handlePreview}
disabled={isDownloading || isDeleting}
title="Preview file"
>
<MdOutlineRemoveRedEye className={styles.actionIcon} />
</button>
<button
className={`${styles.downloadButton} ${isDownloading ? styles.downloading : ''}`}
onClick={() => handleFileDownload(file.id, file.file_name)}
disabled={isDownloading || isDeleting}
title="Download file"
>
<FaDownload className={styles.actionIcon} />
{isDownloading && <span className={styles.actionText}>Downloading...</span>}
</button>
<button
className={`${styles.deleteButton} ${isDeleting ? styles.deleting : ''} ${showDeleteConfirm ? styles.confirm : ''}`}
onClick={handleDeleteClick}
disabled={isDownloading || isDeleting}
title={showDeleteConfirm ? "Click again to confirm deletion" : "Delete file"}
onBlur={handleCancelDelete}
>
<FaTrash className={styles.actionIcon} />
{isDeleting && <span className={styles.actionText}>Deleting...</span>}
{showDeleteConfirm && <span className={styles.actionText}>Click to confirm</span>}
</button>
</div>
</div>
</li>
{/* File Preview Popup */}
{previewDocument && (
<FilePreviewPopup
document={previewDocument}
isOpen={isPreviewOpen}
onClose={handleClosePreview}
/>
)}
</>
);
};

View file

@ -0,0 +1,100 @@
/* No files message */
.noFilesMessage {
display: flex;
justify-content: center;
align-items: center;
padding: 60px 20px;
color: var(--text-color-secondary, #666);
font-style: italic;
}
/* Files table container */
.filesTable {
width: 100%;
margin-top: 10px;
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
height: 100%;
overflow: hidden;
}
/* Table header with exact grid positioning */
.tableHeader {
display: grid;
grid-template-columns: 45% 15% 15% 25%;
align-items: center;
height: 40px;
padding: 0px 16px;
position: sticky;
top: 0;
z-index: 10;
background-color: #fff;
border-bottom: 1px solid #f1f1f1;
margin-bottom: 10px;
flex-shrink: 0;
}
/* Header cells with exact positioning */
.headerCell {
display: flex;
align-items: center;
font-weight: 500;
font-size: 14px;
color: #333;
cursor: pointer;
white-space: nowrap;
padding-left: 0;
transition: color 0.2s ease;
}
.headerCell:hover {
color: var(--Grayscale-Black, #24262B);
}
/* Adjust first column for icon space */
.headerCell:nth-child(1) {
padding-left: 30px;
}
/* Simple sort icon styling */
.sortIcon {
margin-left: 6px;
font-size: 14px;
color: #666;
transition: color 0.2s ease;
}
.headerCell:hover .sortIcon {
color: var(--Grayscale-Black, #24262B);
}
/* File list styling */
.filesList {
list-style: none;
padding: 0;
margin: 0;
width: 100%;
overflow-y: auto;
flex: 1;
min-height: 0;
}
/* Override the flex layout in DateienItem to force matching the header */
.filesList li {
display: grid !important;
grid-template-columns: 45% 15% 15% 25%;
border-bottom: 1px solid #f1f1f1;
height: 60px;
padding: 0 16px;
align-items: center;
transition: background-color 0.2s ease;
position: relative;
}
.filesList li:hover {
background-color: #f9f9f9;
}

View file

@ -0,0 +1,137 @@
import React, { useState } from 'react';
import { FaSort, FaSortUp, FaSortDown } from "react-icons/fa";
import { motion, AnimatePresence } from "framer-motion";
import DateienItem from './DateienItem';
import { UserFile } from '../../hooks/useFiles';
import styles from './DateienLists.module.css';
// Sort types
type SortField = 'file_name' | 'action' | 'size' | 'created_at';
type SortDirection = 'asc' | 'desc';
interface DateienSharedProps {
files: UserFile[];
onFileDeleted: () => void;
onOptimisticDelete?: (fileId: number) => void;
}
const DateienShared: React.FC<DateienSharedProps> = ({ files, onFileDeleted, onOptimisticDelete }) => {
const [sortField, setSortField] = useState<SortField>('created_at');
const [sortDirection, setSortDirection] = useState<SortDirection>('desc');
// Filter files for shared (shared_with_me)
const sharedFiles = files.filter(file => file.source === 'shared_with_me');
// Handle sorting
const handleSort = (field: SortField) => {
if (field === sortField) {
setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc');
} else {
setSortField(field);
setSortDirection(field === 'created_at' ? 'desc' : 'asc');
}
};
// Sort files
const sortedFiles = [...sharedFiles].sort((a, b) => {
let result = 0;
switch (sortField) {
case 'file_name':
result = a.file_name.localeCompare(b.file_name);
break;
case 'action':
result = a.action.localeCompare(b.action);
break;
case 'size':
const sizeA = a.size ?? 0;
const sizeB = b.size ?? 0;
result = sizeA - sizeB;
break;
case 'created_at':
result = new Date(a.created_at).getTime() - new Date(b.created_at).getTime();
break;
}
return sortDirection === 'asc' ? result : -result;
});
// Helper to render sort icon
const renderSortIcon = (field: SortField) => {
if (sortField !== field) return <FaSort className={styles.sortIcon} />;
return sortDirection === 'asc' ?
<FaSortUp className={styles.sortIcon} /> :
<FaSortDown className={styles.sortIcon} />;
};
if (sortedFiles.length === 0) {
return (
<motion.div
className={styles.noFilesMessage}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.3 }}
>
<p>Keine mit Ihnen geteilten Dateien gefunden.</p>
</motion.div>
);
}
return (
<motion.div
className={styles.filesTable}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.3 }}
>
{/* Table Headers */}
<div className={styles.tableHeader}>
<div className={styles.headerCell} onClick={() => handleSort('file_name')}>
<span>Name</span>
{renderSortIcon('file_name')}
</div>
<div className={styles.headerCell} onClick={() => handleSort('action')}>
<span>Typ</span>
{renderSortIcon('action')}
</div>
<div className={styles.headerCell} onClick={() => handleSort('size')}>
<span>Größe</span>
{renderSortIcon('size')}
</div>
<div className={styles.headerCell} onClick={() => handleSort('created_at')}>
<span>Datum</span>
{renderSortIcon('created_at')}
</div>
</div>
{/* Files List with shared indicator */}
<motion.ul
className={`${styles.filesList} ${styles.shared}`}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.4, delay: 0.1 }}
>
<AnimatePresence mode="popLayout">
{sortedFiles.map((file: UserFile) => (
<motion.div
key={file.id}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
layout
>
<DateienItem
file={file}
onDelete={onFileDeleted}
onOptimisticDelete={onOptimisticDelete ? () => onOptimisticDelete(file.id) : undefined}
/>
</motion.div>
))}
</AnimatePresence>
</motion.ul>
</motion.div>
);
};
export default DateienShared;

View file

@ -0,0 +1,137 @@
import React, { useState } from 'react';
import { FaSort, FaSortUp, FaSortDown } from "react-icons/fa";
import { motion, AnimatePresence } from "framer-motion";
import DateienItem from './DateienItem';
import { UserFile } from '../../hooks/useFiles';
import styles from './DateienLists.module.css';
// Sort types
type SortField = 'file_name' | 'action' | 'size' | 'created_at';
type SortDirection = 'asc' | 'desc';
interface DateienUploadsProps {
files: UserFile[];
onFileDeleted: () => void;
onOptimisticDelete?: (fileId: number) => void;
}
const DateienUploads: React.FC<DateienUploadsProps> = ({ files, onFileDeleted, onOptimisticDelete }) => {
const [sortField, setSortField] = useState<SortField>('created_at');
const [sortDirection, setSortDirection] = useState<SortDirection>('desc');
// Filter files for uploads (user_uploaded)
const uploadedFiles = files.filter(file => file.source === 'user_uploaded');
// Handle sorting
const handleSort = (field: SortField) => {
if (field === sortField) {
setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc');
} else {
setSortField(field);
setSortDirection(field === 'created_at' ? 'desc' : 'asc');
}
};
// Sort files
const sortedFiles = [...uploadedFiles].sort((a, b) => {
let result = 0;
switch (sortField) {
case 'file_name':
result = a.file_name.localeCompare(b.file_name);
break;
case 'action':
result = a.action.localeCompare(b.action);
break;
case 'size':
const sizeA = a.size ?? 0;
const sizeB = b.size ?? 0;
result = sizeA - sizeB;
break;
case 'created_at':
result = new Date(a.created_at).getTime() - new Date(b.created_at).getTime();
break;
}
return sortDirection === 'asc' ? result : -result;
});
// Helper to render sort icon
const renderSortIcon = (field: SortField) => {
if (sortField !== field) return <FaSort className={styles.sortIcon} />;
return sortDirection === 'asc' ?
<FaSortUp className={styles.sortIcon} /> :
<FaSortDown className={styles.sortIcon} />;
};
if (sortedFiles.length === 0) {
return (
<motion.div
className={styles.noFilesMessage}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.3 }}
>
<p>Keine hochgeladenen Dateien gefunden.</p>
</motion.div>
);
}
return (
<motion.div
className={styles.filesTable}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.3 }}
>
{/* Table Headers */}
<div className={styles.tableHeader}>
<div className={styles.headerCell} onClick={() => handleSort('file_name')}>
<span>Name</span>
{renderSortIcon('file_name')}
</div>
<div className={styles.headerCell} onClick={() => handleSort('action')}>
<span>Typ</span>
{renderSortIcon('action')}
</div>
<div className={styles.headerCell} onClick={() => handleSort('size')}>
<span>Größe</span>
{renderSortIcon('size')}
</div>
<div className={styles.headerCell} onClick={() => handleSort('created_at')}>
<span>Datum</span>
{renderSortIcon('created_at')}
</div>
</div>
{/* Files List */}
<motion.ul
className={styles.filesList}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.4, delay: 0.1 }}
>
<AnimatePresence mode="popLayout">
{sortedFiles.map((file: UserFile) => (
<motion.div
key={file.id}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
layout
>
<DateienItem
file={file}
onDelete={onFileDeleted}
onOptimisticDelete={onOptimisticDelete ? () => onOptimisticDelete(file.id) : undefined}
/>
</motion.div>
))}
</AnimatePresence>
</motion.ul>
</motion.div>
);
};
export default DateienUploads;

View file

@ -12,6 +12,7 @@ export interface FileInfo {
mandateId?: number;
userId?: number;
workflowId?: string;
source?: string; // 'user_uploaded', 'agent_created', or 'shared_with_me'
}
export interface UserFile {
@ -20,6 +21,7 @@ export interface UserFile {
action: string;
created_at: string;
size?: number;
source?: string; // 'user_uploaded', 'agent_created', or 'shared_with_me'
}
// Files list hook
@ -74,7 +76,8 @@ export function useUserFiles() {
file_name: apiFile.name,
action: action,
created_at: apiFile.creationDate,
size: apiFile.size
size: apiFile.size,
source: apiFile.source
};
});
@ -84,11 +87,28 @@ export function useUserFiles() {
}
};
// Optimistically remove a file from the local state
const removeFileOptimistically = (fileId: number) => {
setFiles(prevFiles => prevFiles.filter(file => file.id !== fileId));
};
// Add a file to the local state (for when upload completes)
const addFileOptimistically = (newFile: UserFile) => {
setFiles(prevFiles => [newFile, ...prevFiles]);
};
useEffect(() => {
fetchFiles();
}, []);
return { files, loading, error, refetch: fetchFiles };
return {
files,
loading,
error,
refetch: fetchFiles,
removeFileOptimistically,
addFileOptimistically
};
}
// File operations hook
@ -136,10 +156,15 @@ export function useFileOperations() {
}
};
const handleFileDelete = async (fileId: number) => {
const handleFileDelete = async (fileId: number, onOptimisticDelete?: () => void) => {
setDeleteError(null);
setDeletingFiles(prev => new Set(prev).add(fileId));
// Optimistically remove from UI if callback provided
if (onOptimisticDelete) {
onOptimisticDelete();
}
try {
await request({
url: `/api/files/${fileId}`,
@ -151,6 +176,7 @@ export function useFileOperations() {
return true;
} catch (error: any) {
setDeleteError(error.message);
// If deletion failed and we optimistically removed it, we should refetch to restore the file
return false;
} finally {
setDeletingFiles(prev => {

View file

@ -156,7 +156,11 @@ export function useWorkflowStatus(workflowId: string | null) {
const { request, isLoading: loading, error } = useApiRequest<null, Workflow>();
const fetchStatus = async () => {
if (!workflowId) return;
if (!workflowId) {
// Clear status when no workflow is selected
setStatus(null);
return;
}
try {
const data = await request({
@ -183,7 +187,11 @@ export function useWorkflowMessages(workflowId: string | null, messageId?: strin
const { request, isLoading: loading, error } = useApiRequest<null, WorkflowMessage[]>();
const fetchMessages = async () => {
if (!workflowId) return;
if (!workflowId) {
// Clear messages when no workflow is selected
setMessages([]);
return;
}
try {
const data = await request({
@ -232,3 +240,95 @@ export function useWorkflowLogs(workflowId: string | null, logId?: string) {
return { logs, loading, error, refetch: fetchLogs };
}
// File preview hook
export function useFilePreview() {
const [previewContent, setPreviewContent] = useState<string | null>(null);
const [fileMetadata, setFileMetadata] = useState<any>(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const { request } = useApiRequest();
const fetchPreview = async (fileId: string | number) => {
console.log('fetchPreview called with fileId:', fileId, 'type:', typeof fileId);
if (!fileId) {
setError("File ID not available");
return;
}
setIsLoading(true);
setError(null);
setPreviewContent(null);
setFileMetadata(null);
try {
// Convert fileId to number since backend expects integer
let numericFileId: number;
if (typeof fileId === 'number') {
numericFileId = fileId;
} else {
numericFileId = parseInt(String(fileId), 10);
}
console.log('Parsed fileId:', numericFileId, 'isNaN:', isNaN(numericFileId));
if (isNaN(numericFileId)) {
throw new Error(`Invalid file ID format: "${fileId}" (type: ${typeof fileId}). Expected a numeric file ID, but got a document UUID. Make sure the document object has a 'fileId' property with the numeric file ID.`);
}
console.log('Making API request to:', `/api/workflows/files/${numericFileId}/preview`);
const response = await request({
url: `/api/workflows/files/${numericFileId}/preview`,
method: 'get'
});
console.log('API response:', response);
// Handle response as object with metadata and preview content
if (typeof response === 'object' && response !== null) {
setFileMetadata(response);
// Try different possible property names for the content
const content = response.preview || response.content || response.data || response.previewContent;
// Debug for PDF issues only
if (response.mimeType === 'application/pdf') {
console.log('PDF Preview Debug:', {
hasPreview: !!response.preview,
previewLength: response.preview?.length,
hasBase64Flag: response.base64Encoded,
mimeType: response.mimeType
});
}
setPreviewContent(content || null);
} else {
// Fallback if response is just the content
setPreviewContent(response);
}
} catch (err: any) {
console.error('File preview error:', err);
setError(err.message || "Failed to load preview");
} finally {
setIsLoading(false);
}
};
const clearPreview = () => {
setPreviewContent(null);
setFileMetadata(null);
setError(null);
setIsLoading(false);
};
return {
previewContent,
fileMetadata,
isLoading,
error,
fetchPreview,
clearPreview
};
}

View file

@ -14,26 +14,28 @@
}
.horizontalLineLight {
width: 100%;
width: calc(100% + 60px);
background-color: #F1F1F1;
height: 1px;
margin-top: 90px;
margin-left: -30px;
position: absolute;
margin-bottom: 0;
flex-shrink: 0;
}
.header {
/* Combined Header with Tabs and Add Button */
.combinedHeader {
display: flex;
gap: 30px;
align-items: flex-start;
height: 62px;
color: var(--Grayscale-Black, #24262B);
padding-top: 30px;
justify-content: space-between;
align-items: center;
flex-shrink: 0;
min-height: 62px;
}
.datei_hinzufügen_button {
border-radius: 30px;
background: var(--Grayscale-Gray, #E9E9E9);
background: #3a8080;
color: #fff;
border: none;
outline: none;
text-align: left;
@ -44,83 +46,77 @@
display: flex;
gap: 10px;
align-items: center;
flex-shrink: 0;
transition: background-color 0.2s ease;
}
.datei_hinzufügen_button:hover {
cursor: pointer;
background-color: #34737b;
}
.add_icon {
font-size: 16px;
}
/* Files table container */
.filesTable {
width: 100%;
margin-top: 20px;
/* Tab Navigation Styles */
.tabButtonDiv {
display: flex;
gap: 12px;
align-items: center;
justify-content: flex-start;
flex: 1;
}
.tabButtonWrapper {
position: relative;
display: flex;
flex-direction: column;
overflow-y: auto;
align-items: center;
}
/* Table header with exact grid positioning to match the screenshot */
.tableHeader {
display: grid;
grid-template-columns: 45% 15% 15% 25%;
align-items: center;
height: 40px;
padding: 0px 16px;
position: sticky;
top: 0;
z-index: 10;
}
/* Header cells with exact positioning */
.headerCell {
display: flex;
align-items: center;
.tabButton {
background: transparent;
border: none;
outline: none;
padding: 20px 0px;
font-size: 14px;
font-weight: 500;
font-size: 14px;
color: #333;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
white-space: nowrap;
padding-left: 0;
position: relative;
}
/* Adjust first column for icon space */
.headerCell:nth-child(1) {
padding-left: 30px;
.tabButtonActive {
color: var(--Grayscale-Black, #24262B);
}
/* Simple sort icon styling */
.sortIcon {
margin-left: 6px;
font-size: 14px;
.tabButtonInactive {
color: #A0A0A0;
}
/* Modify the file list to use the same grid layout */
.filesList {
list-style: none;
padding: 0;
margin: 0;
width: 100%;
.tabButtonInactive:hover {
background-color: var(--background-color-hover, #f5f5f5);
color: var(--text-color-primary, #333);
}
/* Override the flex layout in DateienItem to force matching the header */
.filesList li {
display: grid !important;
grid-template-columns: 45% 15% 15% 25%;
border-bottom: 1px solid #f1f1f1;
height: 60px;
padding: 0 16px;
align-items: center;
.tabUnderline {
position: absolute;
bottom: -2px;
left: 0;
height: 1px;
background-color: var(--Grayscale-Black, #24262B);
border-radius: 1px;
}
.error {
color: #d32f2f;
margin: 1rem 0;
padding: 0.5rem;
background-color: #ffebee;
border-radius: 4px;
text-align: center;
/* Content area */
.contentArea {
flex: 1;
overflow-y: auto;
min-height: 0;
height: calc(100vh - 300px);
display: flex;
flex-direction: column;
}

View file

@ -1,30 +1,30 @@
import styles from './Dateien.module.css'
import { IoAddCircleOutline } from "react-icons/io5";
import { FaSort, FaSortUp, FaSortDown } from "react-icons/fa";
import DateienItem from '../../components/Dateien/DateienItem';
import DateienUpload from './DateienHinzufügen/DateienUpload';
import DateienUpload from '../../components/Dateien/DateienHinzufügen/DateienUploadTool';
import DateienAll from '../../components/Dateien/DateienAll';
import DateienUploads from '../../components/Dateien/DateienUploads';
import DateienCreated from '../../components/Dateien/DateienCreated';
import DateienShared from '../../components/Dateien/DateienShared';
import { useState } from 'react';
import { useUserFiles, useFileOperations } from '../../hooks/useFiles';
import { motion, AnimatePresence } from "framer-motion";
// Define the file type interface
interface UserFile {
id: number;
file_name: string;
action: string;
created_at: string;
size?: number;
}
// Sort types
type SortField = 'file_name' | 'action' | 'size' | 'created_at';
type SortDirection = 'asc' | 'desc';
// Tab types
type TabType = 'alle' | 'uploads' | 'erstellt' | 'geteilt';
function Dateien() {
const { files, loading, error, refetch } = useUserFiles();
const { files, loading, error, refetch, removeFileOptimistically } = useUserFiles();
const [isUploadOpen, setIsUploadOpen] = useState(false);
const { uploadError, downloadError, deleteError } = useFileOperations();
const [sortField, setSortField] = useState<SortField>('created_at');
const [sortDirection, setSortDirection] = useState<SortDirection>('desc');
const [activeTab, setActiveTab] = useState<TabType>('alle');
// Tab configuration
const tabs = [
{ key: 'alle' as TabType, label: 'Alle Dateien' },
{ key: 'uploads' as TabType, label: 'Meine Uploads' },
{ key: 'erstellt' as TabType, label: 'Erstellte Dateien' },
{ key: 'geteilt' as TabType, label: 'Geteilte Dateien' }
];
// Single function to handle file refresh
const refreshFiles = () => {
@ -48,55 +48,65 @@ function Dateien() {
refreshFiles();
};
// Handle sorting
const handleSort = (field: SortField) => {
if (field === sortField) {
// Toggle direction if same field is clicked
setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc');
} else {
// Set new field and default to ascending for most fields, descending for dates
setSortField(field);
setSortDirection(field === 'created_at' ? 'desc' : 'asc');
// Render the appropriate component based on active tab
const renderActiveTabContent = () => {
const commonProps = {
files,
onFileDeleted: handleFileDeleted,
onOptimisticDelete: removeFileOptimistically
};
switch (activeTab) {
case 'alle':
return <DateienAll {...commonProps} />;
case 'uploads':
return <DateienUploads {...commonProps} />;
case 'erstellt':
return <DateienCreated {...commonProps} />;
case 'geteilt':
return <DateienShared {...commonProps} />;
default:
return <DateienAll {...commonProps} />;
}
};
// Sort files
const sortedFiles = [...files].sort((a, b) => {
let result = 0;
switch (sortField) {
case 'file_name':
result = a.file_name.localeCompare(b.file_name);
break;
case 'action':
result = a.action.localeCompare(b.action);
break;
case 'size':
// Handle undefined sizes gracefully
const sizeA = a.size ?? 0;
const sizeB = b.size ?? 0;
result = sizeA - sizeB;
break;
case 'created_at':
result = new Date(a.created_at).getTime() - new Date(b.created_at).getTime();
break;
}
// Apply sort direction
return sortDirection === 'asc' ? result : -result;
});
// Helper to render sort icon
const renderSortIcon = (field: SortField) => {
if (sortField !== field) return <FaSort className={styles.sortIcon} />;
return sortDirection === 'asc' ?
<FaSortUp className={styles.sortIcon} /> :
<FaSortDown className={styles.sortIcon} />;
};
return (
<div className={styles.dateienContainer}>
<div className={styles.header}>
{/* Combined Header with Tabs and Add Button */}
<motion.div
className={styles.combinedHeader}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.3 }}
>
<div className={styles.tabButtonDiv}>
{tabs.map((tab) => (
<div key={tab.key} className={styles.tabButtonWrapper}>
<motion.button
className={`${styles.tabButton} ${
activeTab === tab.key ? styles.tabButtonActive : styles.tabButtonInactive
}`}
onClick={() => setActiveTab(tab.key)}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
{tab.label}
</motion.button>
<AnimatePresence>
{activeTab === tab.key && (
<motion.div
className={styles.tabUnderline}
initial={{ opacity: 0, width: "0%" }}
animate={{ opacity: 1, width: "100%" }}
exit={{ opacity: 0, width: "0%" }}
transition={{ duration: 0.3, ease: "easeOut" }}
/>
)}
</AnimatePresence>
</div>
))}
</div>
<button
className={styles.datei_hinzufügen_button}
onClick={() => setIsUploadOpen(true)}
@ -104,60 +114,37 @@ function Dateien() {
<IoAddCircleOutline className={styles.add_icon}/>
Datei hinzufügen
</button>
</div>
</motion.div>
<div className={styles.horizontalLineLight}></div>
<DateienUpload
isOpen={isUploadOpen}
onClose={handleUploadClose}
onFileUpload={handleFileUpload}
/>
<div className={styles.contentArea}>
<DateienUpload
isOpen={isUploadOpen}
onClose={handleUploadClose}
onFileUpload={handleFileUpload}
/>
{(uploadError || downloadError || deleteError) && (
<p className={styles.error}>
{uploadError || downloadError || deleteError}
</p>
)}
{loading && <p>Loading files...</p>}
{error && <p>Error: {error}</p>}
{(uploadError || downloadError || deleteError) && (
<p className={styles.error}>
{uploadError || downloadError || deleteError}
</p>
)}
{!loading && !error && files.length === 0 ? (
<p>No files found.</p>
) : (
<div className={styles.filesTable}>
{/* Table Headers */}
<div className={styles.tableHeader}>
<div className={styles.headerCell} onClick={() => handleSort('file_name')}>
<span>Name</span>
{renderSortIcon('file_name')}
</div>
<div className={styles.headerCell} onClick={() => handleSort('action')}>
<span>Typ</span>
{renderSortIcon('action')}
</div>
<div className={styles.headerCell} onClick={() => handleSort('size')}>
<span>Größe</span>
{renderSortIcon('size')}
</div>
<div className={styles.headerCell} onClick={() => handleSort('created_at')}>
<span>Datum</span>
{renderSortIcon('created_at')}
</div>
{loading && <p>Loading files...</p>}
{error && <p>Error: {error}</p>}
</div>
{/* Files List */}
<ul className={styles.filesList}>
{sortedFiles.map((file: UserFile) => (
<DateienItem
key={file.id}
file={file}
onDelete={handleFileDeleted}
/>
))}
</ul>
</div>
)}
{!loading && !error && (
<motion.div
key={activeTab} // Force re-render when tab changes
initial={{ opacity: 0}}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.3 }}
>
{renderActiveTabContent()}
</motion.div>
)}
</div>
</div>
);
}