diff --git a/.gitignore b/.gitignore index 5cbc969..7d8652d 100644 --- a/.gitignore +++ b/.gitignore @@ -26,4 +26,9 @@ dist-ssr # Environment files .env .env.local -.env.*.local \ No newline at end of file +<<<<<<< Updated upstream +.env.*.local +======= +.env.*.local + +>>>>>>> Stashed changes diff --git a/package.json b/package.json index 379b850..e894238 100644 --- a/package.json +++ b/package.json @@ -1,16 +1,16 @@ { - "name": "frontend", + "name": "frontend_nyla_new", "private": true, "version": "0.0.0", "type": "module", "scripts": { "dev": "vite --port 5176", - "build": "vite build", + "build": "tsc -b && vite build", "lint": "eslint .", - "preview": "vite preview", - "start": "node server.js" + "preview": "vite preview" }, "dependencies": { +<<<<<<< Updated upstream "@azure/msal-browser": "^4.12.0", "@azure/msal-react": "^3.0.12", "@xstate/react": "^5.0.0", @@ -24,23 +24,22 @@ "jwt-decode": "^4.0.0", "motion": "^12.7.3", "pg": "^8.8.0", +======= +>>>>>>> Stashed changes "react": "^19.1.0", - "react-dom": "^19.1.0", - "react-dropzone": "^14.3.8", - "react-icons": "^5.5.0", - "react-markdown": "^10.1.0", - "react-router-dom": "^7.5.0", - "xstate": "^5.18.0" + "react-dom": "^19.1.0" }, "devDependencies": { - "@eslint/js": "^9.21.0", - "@types/react": "^19.1.6", - "@types/react-dom": "^19.1.5", - "@vitejs/plugin-react": "^4.3.4", - "eslint": "^9.21.0", - "eslint-plugin-react-hooks": "^5.1.0", - "eslint-plugin-react-refresh": "^0.4.19", - "globals": "^15.15.0", - "vite": "^6.2.0" + "@eslint/js": "^9.30.1", + "@types/react": "^19.1.8", + "@types/react-dom": "^19.1.6", + "@vitejs/plugin-react": "^4.6.0", + "eslint": "^9.30.1", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.20", + "globals": "^16.3.0", + "typescript": "~5.8.3", + "typescript-eslint": "^8.35.1", + "vite": "^5.4.10" } } diff --git a/src/assets/styles/light.css b/src/assets/styles/light.css index 14a6616..861531f 100644 --- a/src/assets/styles/light.css +++ b/src/assets/styles/light.css @@ -1,15 +1,15 @@ :root { - --color-bg: #FFFFFF; - --color-surface: #F8F9FA; - --color-text: #24262B; + --color-bg: #F8F9FA; /* war vorher surface */ + --color-surface: #EFEDE5; /* war vorher bg */ + --color-text: #181818; - --color-primary: #8F00FF; - --color-primary-hover: #A020FF; - --color-primary-disabled: #D1A6F9; + --color-primary: #C7C5B2; + --color-primary-hover: #D9D7C6; + --color-primary-disabled: #E3E2D8; - --color-secondary: #3F51B5; - --color-secondary-hover: #5A6CE0; - --color-secondary-disabled: #BEC5EB; + --color-secondary: #F25843; + --color-secondary-hover: #FF6A55; + --color-secondary-disabled: #F5B0A4; --color-red: #D85B65; --color-red-hover: #E77A81; @@ -19,26 +19,26 @@ --color-secondary-red-hover: #D46872; --color-secondary-red-disabled: #E8B7BA; - --color-gray: #6C757D; - --color-gray-hover: #8A9299; - --color-gray-disabled: #D6D8DB; + --color-gray: #181818; + --color-gray-hover: #2A2A2A; + --color-gray-disabled: #9B9B9B; - --font-family: "Trebuchet MS", sans-serif; -} - -/* Dark theme overrides */ -.dark-theme { - --color-bg: #121212; - --color-surface: #1E1E1E; + --font-family: "DM Sans", sans-serif; + } + + /* Dark theme overrides */ + .dark-theme { + --color-bg: #181818; /* war vorher surface */ + --color-surface: #1E1D1A; /* war vorher bg */ --color-text: #E5E7EB; - --color-primary: #B266FF; - --color-primary-hover: #C68AFF; - --color-primary-disabled: #5C2B80; + --color-primary: #C7C5B2; + --color-primary-hover: #E0DECC; + --color-primary-disabled: #59584F; - --color-secondary: #6F7BE5; - --color-secondary-hover: #8592FF; - --color-secondary-disabled: #3B4370; + --color-secondary: #F25843; + --color-secondary-hover: #FF715C; + --color-secondary-disabled: #6E3E36; --color-red: #FF6F7A; --color-red-hover: #FF8B94; @@ -48,8 +48,8 @@ --color-secondary-red-hover: #E17683; --color-secondary-red-disabled: #70363C; - --color-gray: #A0A4AA; - --color-gray-hover: #C4C8CD; - --color-gray-disabled: #505357; -} + --color-gray: #181818; + --color-gray-hover: #2E2E2E; + --color-gray-disabled: #505050; + } \ No newline at end of file diff --git a/src/components/Dashboard/DashboardChat/DashboardChat.module.css b/src/components/Dashboard/DashboardChat/DashboardChat.module.css index 8fc3ff8..3df453d 100644 --- a/src/components/Dashboard/DashboardChat/DashboardChat.module.css +++ b/src/components/Dashboard/DashboardChat/DashboardChat.module.css @@ -1,317 +1,16 @@ .dashboard_chat { display: flex; padding: 20px; - flex-direction: column; + flex-direction: column; /* Fixed: was 'space-between' which is invalid */ align-self: stretch; - border-radius: 30px; background: var(--color-bg); position: relative; box-shadow: 0px 2px 6px 0px rgba(194, 194, 194, 0.10); - height: 100%; min-height: 0; + height: 100%; /* Fill parent height */ + flex: 1; /* Take all available space from parent */ overflow: hidden; font-family: var(--font-family); } -.dashboard_chat.expanded { - width: 100%; -} - -.chat_header { - display: flex; - justify-content: space-between; - align-items: flex-start; - flex-shrink: 0; -} - -.chat_button_div { - display: flex; - gap: 20px; - align-items: flex-start; -} - -.buttonWrapper { - display: flex; - flex-direction: column; - position: relative; -} - -.chat_button { - text-align: center; - font-size: 14px; - font-style: normal; - font-weight: 500; - line-height: normal; - border: none; - background: none; - outline: none; - cursor: pointer; - padding: 0; - transition: all 0.2s ease; - font-family: var(--font-family); -} - -.chat_button_active { - color: var(--color-text); -} - -.chat_button_inactive { - color: var(--color-gray); -} - -.chat_button_collapsed { - opacity: 50%; - color: var(--color-gray); -} - -.iconContainer { - display: flex; - gap: 10px; - align-items: center; -} - -.expandIcon, .collapseIcon { - cursor: pointer; - display: flex; - align-items: center; - color: var(--color-secondary); -} - -.expandIcon:hover, .collapseIcon:hover { - color: var(--color-secondary-hover); -} - -.horizontalLine { - width: 100%; - background-color: var(--color-text); - height: 1px; - margin-top: 20px; -} - -.horizontalLineLight { - width: calc(100%); - background-color: var(--color-gray-disabled); - height: 2px; - margin-top: 39px; - margin-left: -20px; - position: absolute; - flex-shrink: 0; -} - -.chat_content { - display: flex; - flex-direction: column; - flex: 1; - margin-top: 20px; - min-height: 0; - overflow: hidden; -} - -.chat_messages { - flex: 1; - padding: 15px; - border-radius: 15px; - margin-bottom: 15px; - min-height: 200px; - overflow-y: auto; - overflow-x: hidden; - scroll-behavior: smooth; -} - -.chat_input { - display: flex; - gap: 10px; - align-items: center; - flex-shrink: 0; -} - -.message_input { - flex: 1; - padding: 12px 16px; - border-radius: 12px; - outline: none; - font-size: 14px; - font-family: var(--font-family); - background-color: var(--color-bg); - color: var(--color-text); -} - -.message_input:focus { - border-color: var(--color-primary); -} - -.send_button { - padding: 12px 12px; - background-color: var(--color-secondary); - color: var(--color-bg); - border: none; - border-radius: 12px; - cursor: pointer; - font-size: 14px; - font-weight: 500; - display: flex; - align-items: center; - justify-content: center; - font-family: var(--font-family); -} - -.send_button_icon { - height: 100%; - width: 100%; - margin: none; - padding: none; -} - -.send_button:disabled { - background-color: var(--color-gray-disabled); - cursor: not-allowed; - opacity: 0.6; -} - -.message_input:disabled { - background-color: var(--color-surface); - cursor: not-allowed; - opacity: 0.6; -} - -.loading_message { - padding: 10px; - background-color: var(--color-secondary-disabled); - border-left: 4px solid var(--color-secondary); - border-radius: 4px; - margin-bottom: 10px; -} - -.loading_message p { - margin: 0; - color: var(--color-secondary); - font-size: 14px; - font-family: var(--font-family); -} - -.error_message { - padding: 10px; - background-color: var(--color-red-disabled); - border-left: 4px solid var(--color-red); - border-radius: 4px; - margin-bottom: 10px; -} - -.error_message p { - margin: 0; - color: var(--color-red); - font-size: 14px; - font-family: var(--font-family); -} - -.message { - margin-bottom: 15px; - padding: 12px; - border-radius: 12px; - max-width: 80%; - font-family: var(--font-family); -} - -.message_user { - background-color: var(--color-secondary); - color: var(--color-bg); - margin-left: auto; - margin-right: 0; -} - -.message_assistant { - background-color: var(--color-surface); - color: var(--color-text); - margin-left: 0; - margin-right: auto; -} - -.message_system { - background-color: var(--color-primary-disabled); - color: var(--color-primary); - margin-left: auto; - margin-right: auto; - text-align: center; -} - -.message_role { - font-size: 12px; - font-weight: 600; - margin-bottom: 4px; - opacity: 0.8; - font-family: var(--font-family); -} - -.message_content { - font-size: 14px; - line-height: 1.4; - white-space: pre-wrap; - word-wrap: break-word; - font-family: var(--font-family); -} - -.message_timestamp { - font-size: 11px; - margin-top: 4px; - opacity: 0.6; - font-family: var(--font-family); -} - -.placeholder_text { - text-align: center; - color: var(--color-gray); - font-style: italic; - margin: 20px 0; - font-family: var(--font-family); -} - -.workflow_status { - padding: 8px 12px; - background-color: var(--color-secondary-disabled); - border-left: 4px solid var(--color-secondary); - border-radius: 4px; - margin-bottom: 10px; -} - -.workflow_status p { - margin: 0; - color: var(--color-secondary); - font-size: 13px; - font-style: italic; - font-family: var(--font-family); -} - -.completion_message { - padding: 10px 12px; - background-color: var(--color-secondary-disabled); - border-left: 4px solid var(--color-secondary); - border-radius: 4px; - margin-bottom: 10px; - text-align: center; -} - -.completion_message p { - margin: 0 0 10px 0; - color: var(--color-secondary); - font-size: 14px; - font-weight: 600; - font-family: var(--font-family); -} - -.new_workflow_button { - background-color: var(--color-secondary); - color: var(--color-bg); - border: none; - border-radius: 8px; - padding: 8px 16px; - font-size: 12px; - font-weight: 500; - cursor: pointer; - transition: background-color 0.2s ease; - font-family: var(--font-family); -} - -.new_workflow_button:hover { - background-color: var(--color-secondary-hover); -} diff --git a/src/components/Dashboard/DashboardChat/DashboardChat.tsx b/src/components/Dashboard/DashboardChat/DashboardChat.tsx index 1c90e5b..79d79b7 100644 --- a/src/components/Dashboard/DashboardChat/DashboardChat.tsx +++ b/src/components/Dashboard/DashboardChat/DashboardChat.tsx @@ -48,91 +48,15 @@ const DashboardChat: React.FC = ({ }; return ( - - -
- {[t('dashboard.chat.area'), t('dashboard.chat.history')].map((tab) => ( -
- setActiveTab(tab)} - whileHover={{ scale: 1.05 }} - whileTap={{ scale: 0.98 }} - > - {tab} - - - {activeTab === tab && ( - - )} - -
- ))} -
-
- - {isExpanded ? : } - -
-
- - - {activeTab === t('dashboard.chat.area') ? ( - - ) : ( - - )} - -
+
+ +
); }; diff --git a/src/components/Dashboard/DashboardChat/DashboardChatArea/DashboardChatArea.tsx b/src/components/Dashboard/DashboardChat/DashboardChatArea/DashboardChatArea.tsx index e842786..3245212 100644 --- a/src/components/Dashboard/DashboardChat/DashboardChatArea/DashboardChatArea.tsx +++ b/src/components/Dashboard/DashboardChat/DashboardChatArea/DashboardChatArea.tsx @@ -1,10 +1,10 @@ -import React, { useEffect } from "react"; -import { DashboardChatAreaProps } from "./dashboardChatAreaTypes"; -import { useChatLogic } from "./dashboardChatAreaLogic"; -import { useLanguage } from "../../../../contexts/LanguageContext"; +import React, { useState } from "react"; import MessageList from "./DashboardChatAreaMessageList"; -import ChatInput from "./DashboardChatAreaInput"; -import styles from './DashboardChatArea.module.css'; +import FilePreview from "./DashboardChatAreaFilePreview"; +import InputArea from "./DashboardChatAreaInput"; +import ConnectedFiles from "./DashboardChatAreaConnectedFiles"; +import "./DashboardChatAreaStyles/grid.css"; +import { DashboardChatAreaProps } from "./dashboardChatAreaTypes"; const DashboardChatArea: React.FC = ({ selectedPrompt, @@ -13,92 +13,134 @@ const DashboardChatArea: React.FC = ({ onWorkflowCompletedChange, resumeWorkflowId }) => { - const { - // State - inputValue, - setInputValue, - currentWorkflowId, - workflowCompleted, - attachedFiles, - - // Refs - inputRef, - messagesEndRef, - - // Data from hooks - messages, - messagesLoading, - messagesError, - startingWorkflow, - startError, - workflowStatus, - - // Handlers - handleSend, - handleKeyPress, - startNewWorkflow, - handleStopWorkflow, - handleFileAttach, - handleFileRemove, - handleFilesSelect, - handleRetry, - - // Workflow state - isWorkflowRunning, - isStoppingWorkflow, - shouldShowRetryButton - } = useChatLogic({ - selectedPrompt, - onPromptUsed, - onWorkflowIdChange, - resumeWorkflowId - }); + // Grid sizing state + const [horizontalSplit, setHorizontalSplit] = useState(60); // percentage + const [verticalSplit, setVerticalSplit] = useState(60); // percentage + const [isDragging, setIsDragging] = useState<'horizontal' | 'vertical' | null>(null); - const { t } = useLanguage(); + // File selection state + const [selectedFile, setSelectedFile] = useState(null); + const [attachedFiles, setAttachedFiles] = useState([]); + + // Workflow state + const [currentWorkflowId, setCurrentWorkflowId] = useState(resumeWorkflowId || null); - // Notify parent component when workflow completion status changes - useEffect(() => { - if (onWorkflowCompletedChange) { - onWorkflowCompletedChange(workflowCompleted); + // Handle workflow ID changes + const handleWorkflowIdChange = (workflowId: string | null) => { + setCurrentWorkflowId(workflowId); + if (onWorkflowIdChange) { + onWorkflowIdChange(workflowId); } - }, [workflowCompleted, onWorkflowCompletedChange]); + }; - const placeholder = workflowCompleted ? t('chat.continue_conversation') : t('chat.enter_message'); + // Handle resizing + const handleMouseDown = (direction: 'horizontal' | 'vertical') => (e: React.MouseEvent) => { + e.preventDefault(); + setIsDragging(direction); + }; + + const handleMouseMove = (e: MouseEvent) => { + if (!isDragging) return; + + const container = document.querySelector('.chat-grid') as HTMLElement; + if (!container) return; + + const rect = container.getBoundingClientRect(); + + if (isDragging === 'horizontal') { + const newSplit = ((e.clientY - rect.top) / rect.height) * 100; + setHorizontalSplit(Math.max(20, Math.min(80, newSplit))); + } else if (isDragging === 'vertical') { + const newSplit = ((e.clientX - rect.left) / rect.width) * 100; + setVerticalSplit(Math.max(20, Math.min(80, newSplit))); + } + }; + + const handleMouseUp = () => { + setIsDragging(null); + }; + + // Event listeners + React.useEffect(() => { + if (isDragging) { + document.addEventListener('mousemove', handleMouseMove); + document.addEventListener('mouseup', handleMouseUp); + document.body.style.cursor = isDragging === 'horizontal' ? 'ns-resize' : 'ew-resize'; + document.body.style.userSelect = 'none'; + + return () => { + document.removeEventListener('mousemove', handleMouseMove); + document.removeEventListener('mouseup', handleMouseUp); + document.body.style.cursor = ''; + document.body.style.userSelect = ''; + }; + } + }, [isDragging]); return ( -
- + {/* Top Left: Message List */} +
+ +
+ + {/* Vertical Divider */} +
- + +
+ + {/* Horizontal Divider */} +
+ + {/* Bottom Left: Input Area */} +
+ +
+ + {/* Bottom Right: Connected Files */} +
+ { + // If the removed file is currently selected, clear the selection + if (selectedFile?.id === fileId) { + setSelectedFile(null); + } + // Remove the file from attached files + setAttachedFiles(files => files.filter(f => f.id !== fileId)); + }} + /> +
); }; -export default DashboardChatArea; \ No newline at end of file +export default DashboardChatArea; diff --git a/src/components/Dashboard/DashboardChat/DashboardChatArea/DashboardChatAreaConnectedFiles.tsx b/src/components/Dashboard/DashboardChat/DashboardChatArea/DashboardChatAreaConnectedFiles.tsx new file mode 100644 index 0000000..76b74c9 --- /dev/null +++ b/src/components/Dashboard/DashboardChat/DashboardChatArea/DashboardChatAreaConnectedFiles.tsx @@ -0,0 +1,208 @@ +import React, { useState, useEffect } from 'react'; +import { useFileDownload } from '../../../../hooks/useWorkflows'; +import { FileInfo } from './dashboardChatAreaTypes'; + +interface AttachedFile { + id: number; + name: string; + size: number; + type: string; + fileData?: File; + objectUrl?: string; +} + +interface ConnectedFilesProps { + onFileSelect?: (file: FileInfo) => void; + selectedFile?: FileInfo | null; + attachedFiles?: AttachedFile[]; + onRemoveFile?: (fileId: number) => void; +} + +const ConnectedFiles: React.FC = ({ + onFileSelect, + selectedFile, + attachedFiles = [], + onRemoveFile +}) => { + const [files, setFiles] = useState([]); + const { downloadFile, isDownloading } = useFileDownload(); + + // Convert attached files to FileInfo format for compatibility with preview + const convertedAttachedFiles = attachedFiles.map(file => { + console.log('ConnectedFiles: Converting attached file:', file.name, 'Has fileData:', !!file.fileData, 'Has objectUrl:', !!file.objectUrl); + return { + id: file.id, + name: file.name, + mimeType: file.type, + size: file.size, + creationDate: new Date().toISOString(), + fileData: file.fileData, + objectUrl: file.objectUrl + }; + }); + + // Combine attached files with workflow files + const allFiles = [...convertedAttachedFiles, ...files]; + + useEffect(() => { + // Could load workflow-specific files here in the future + }, []); + + const getFileIcon = (mimeType: string) => { + if (mimeType.includes('pdf')) return 'πŸ“„'; + if (mimeType.includes('spreadsheet') || mimeType.includes('excel')) return 'πŸ“Š'; + if (mimeType.startsWith('image/')) return 'πŸ–ΌοΈ'; + if (mimeType.startsWith('text/')) return 'πŸ“'; + return 'πŸ“Ž'; + }; + + const formatFileSize = (bytes: number) => { + if (bytes < 1024) return bytes + ' B'; + if (bytes < 1024 * 1024) return Math.round(bytes / 1024) + ' KB'; + return Math.round(bytes / (1024 * 1024)) + ' MB'; + }; + + const handleFileClick = (file: any) => { + if (onFileSelect) { + console.log('ConnectedFiles: Selecting file:', file.name, 'Has fileData:', !!file.fileData, 'Has objectUrl:', !!file.objectUrl); + onFileSelect(file); + } + }; + + const handleDownload = async (file: FileInfo) => { + await downloadFile(file.id, file.name); + }; + + return ( +
+

Connected Files

+ + {/* Show attached files count */} + {attachedFiles.length > 0 && ( +
+ πŸ“Ž {attachedFiles.length} file{attachedFiles.length !== 1 ? 's' : ''} attached for workflow +
+ )} + + {allFiles.length === 0 ? ( +

+ No files connected to this workflow +

+ ) : ( +
+ {allFiles.map((file) => { + const isAttachedFile = attachedFiles.some(af => af.id === file.id); + return ( +
handleFileClick(file)} + style={{ + padding: '12px', + border: `1px solid ${selectedFile?.id === file.id ? 'var(--color-secondary)' : 'var(--color-gray-disabled)'}`, + borderRadius: '8px', + cursor: 'pointer', + backgroundColor: selectedFile?.id === file.id ? 'var(--color-secondary-disabled)' : 'var(--color-bg)', + display: 'flex', + alignItems: 'center', + gap: '12px', + // Highlight attached files + ...(isAttachedFile && { + borderColor: '#1976d2', + backgroundColor: '#f3f8ff' + }) + }} + > + + {getFileIcon(file.mimeType)} + + +
+
+ {file.name} + {isAttachedFile && ( + + ATTACHED + + )} +
+
+ {file.size ? formatFileSize(file.size) : 'Unknown size'} +
+
+ +
+ {isAttachedFile && onRemoveFile && ( + + )} + +
+
+ ); + })} +
+ )} +
+ ); +}; + +export default ConnectedFiles; diff --git a/src/components/Dashboard/DashboardChat/DashboardChatArea/DashboardChatAreaFilePreview.tsx b/src/components/Dashboard/DashboardChat/DashboardChatArea/DashboardChatAreaFilePreview.tsx new file mode 100644 index 0000000..f909faf --- /dev/null +++ b/src/components/Dashboard/DashboardChat/DashboardChatArea/DashboardChatAreaFilePreview.tsx @@ -0,0 +1,192 @@ +import React from 'react'; +import { useFilePreview } from '../../../../hooks/useWorkflows'; +import { FileInfo } from './dashboardChatAreaTypes'; + +interface AttachedFileWithData extends FileInfo { + fileData?: File; + objectUrl?: string; +} + +interface FilePreviewProps { + selectedFile?: AttachedFileWithData | null; +} + +const FilePreview: React.FC = ({ selectedFile }) => { + const { previewContent, fileMetadata, isLoading, error, fetchPreview } = useFilePreview(); + const [imageUrl, setImageUrl] = React.useState(null); + + // Handle base64 image data from backend + React.useEffect(() => { + if (fileMetadata && fileMetadata.base64Encoded && fileMetadata.preview) { + const isImage = fileMetadata.mimeType?.startsWith('image/') || + selectedFile?.name?.toLowerCase().match(/\.(png|jpg|jpeg|gif|bmp|webp)$/); + + if (isImage) { + + const dataUrl = `data:${fileMetadata.mimeType || 'image/png'};base64,${fileMetadata.preview}`; + setImageUrl(dataUrl); + + } + } + }, [fileMetadata, selectedFile]); + + React.useEffect(() => { + // Clean up previous object URL + const currentImageUrl = imageUrl; + if (currentImageUrl && currentImageUrl.startsWith('blob:')) { + URL.revokeObjectURL(currentImageUrl); + } + setImageUrl(null); + + if (selectedFile?.id) { + + // Check if it's an image file (either from mimeType or file extension) + const isImage = selectedFile.mimeType?.startsWith('image/') || + selectedFile.name?.toLowerCase().match(/\.(png|jpg|jpeg|gif|bmp|webp)$/); + + if (isImage) { + // If it's an attached file with file data, create object URL for preview + if (selectedFile.fileData) { + const url = URL.createObjectURL(selectedFile.fileData); + setImageUrl(url); + } else if (selectedFile.objectUrl) { + setImageUrl(selectedFile.objectUrl); + } else if (selectedFile.downloadUrl) { + setImageUrl(selectedFile.downloadUrl); + } else { + // For existing uploaded files, fetch the image data + fetchPreview(selectedFile.id); + } + } else { + // For non-image files, try to fetch preview + fetchPreview(selectedFile.id); + } + } + + // Cleanup function + return () => { + if (currentImageUrl && currentImageUrl.startsWith('blob:')) { + URL.revokeObjectURL(currentImageUrl); + } + }; + }, [selectedFile?.id, selectedFile?.fileData, selectedFile?.objectUrl]); + + // Cleanup on unmount + React.useEffect(() => { + return () => { + if (imageUrl) { + URL.revokeObjectURL(imageUrl); + } + }; + }, []); + + const getFileType = (mimeType?: string) => { + if (!mimeType) return 'Unknown'; + if (mimeType.startsWith('image/')) return 'Image'; + if (mimeType.startsWith('text/')) return 'Text'; + if (mimeType.includes('pdf')) return 'PDF'; + if (mimeType.includes('spreadsheet') || mimeType.includes('excel')) return 'Spreadsheet'; + return 'Document'; + }; + + return ( +
+

File Preview

+ + {!selectedFile && ( +

+ Select a file to preview +

+ )} + + {selectedFile && ( +
+
+

{selectedFile.name}

+
+ Type: {getFileType(selectedFile.mimeType)} β€’ + Size: {selectedFile.size ? Math.round(selectedFile.size / 1024) + ' KB' : 'Unknown'} +
+
+ + {/* Image Preview - Show first for images */} + {(selectedFile.mimeType?.startsWith('image/') || selectedFile.name?.toLowerCase().match(/\.(png|jpg|jpeg|gif|bmp|webp)$/)) && imageUrl ? ( +
+ {selectedFile.name} console.log('Image loaded successfully')} + onError={(e) => { + console.error('Image failed to load:', e); + console.log('Image URL:', imageUrl); + }} + /> +
+ ) : (selectedFile.mimeType?.startsWith('image/') || selectedFile.name?.toLowerCase().match(/\.(png|jpg|jpeg|gif|bmp|webp)$/)) ? ( +
+

πŸ–ΌοΈ Image preview loading...

+ + Debug: imageUrl={imageUrl ? 'yes' : 'no'}, downloadUrl={selectedFile.downloadUrl ? 'yes' : 'no'}, + fileData={selectedFile.fileData ? 'yes' : 'no'}, objectUrl={selectedFile.objectUrl ? 'yes' : 'no'} +
+ MimeType: {selectedFile.mimeType} +
+
+ ) : null} + + {/* Text/Code Preview - Only for non-images and when we don't have an image URL */} + {!(selectedFile.mimeType?.startsWith('image/') || selectedFile.name?.toLowerCase().match(/\.(png|jpg|jpeg|gif|bmp|webp)$/)) && !imageUrl && ( + <> + {isLoading &&

Loading preview...

} + {error &&

Error: {error}

} + + {previewContent && ( +
+ {previewContent} +
+ )} + + {!previewContent && !isLoading && !error && ( +

+ Preview not available for this file type +

+ )} + + )} +
+ )} +
+ ); +}; + +export default FilePreview; diff --git a/src/components/Dashboard/DashboardChat/DashboardChatArea/DashboardChatAreaInput.tsx b/src/components/Dashboard/DashboardChat/DashboardChatArea/DashboardChatAreaInput.tsx index f0cfc40..106715d 100644 --- a/src/components/Dashboard/DashboardChat/DashboardChatArea/DashboardChatAreaInput.tsx +++ b/src/components/Dashboard/DashboardChat/DashboardChatArea/DashboardChatAreaInput.tsx @@ -1,239 +1,187 @@ -import React, { useState, useEffect } from "react"; -import { motion } from "framer-motion"; -import { LuSendHorizontal } from "react-icons/lu"; -import { FaStop } from "react-icons/fa"; -import { IoAttach, IoClose } from "react-icons/io5"; -import { ChatInputProps } from "./dashboardChatAreaTypes"; -import { FileInfo } from "../../../../hooks/useFiles"; -import { useLanguage } from "../../../../contexts/LanguageContext"; -import DateienSelector from "../../../Dateien/DateienHinzufΓΌgen/DateienSelector"; -import styles from './DashboardChatArea.module.css'; +import React, { useState, useEffect } from 'react'; +import { useWorkflowOperations } from '../../../../hooks/useWorkflows'; +import { Prompt } from '../../../../hooks/usePrompts'; +import FileAttachmentPopup from './FileAttachmentPopup'; -// 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 'πŸ“„'; -}; +interface InputAreaProps { + selectedPrompt?: Prompt | null; + onPromptUsed?: () => void; + onWorkflowIdChange?: (workflowId: string | null) => void; + onAttachedFilesChange?: (files: AttachedFile[]) => void; + attachedFiles?: AttachedFile[]; +} -const ChatInput: React.FC = ({ - inputValue, - setInputValue, - onSend, - onKeyPress, - isDisabled, - placeholder, - inputRef, - isWorkflowRunning, - onStopWorkflow, - isStoppingWorkflow, - attachedFiles, - onFileAttach, - onFileRemove, - onFilesSelect +interface AttachedFile { + id: number; + name: string; + size: number; + type: string; + fileData?: File; + objectUrl?: string; +} + +const InputArea: React.FC = ({ + selectedPrompt, + onPromptUsed, + onWorkflowIdChange, + onAttachedFilesChange, + attachedFiles: externalAttachedFiles = [] }) => { - const { t } = useLanguage(); - const [isUploadModalOpen, setIsUploadModalOpen] = useState(false); - const [isDragOver, setIsDragOver] = useState(false); + const [inputValue, setInputValue] = useState(''); + const [showFilePopup, setShowFilePopup] = useState(false); - // Auto-resize textarea functionality + // Always use external attached files from parent component + const currentAttachedFiles = externalAttachedFiles; + const { startWorkflow, startingWorkflow, startError } = useWorkflowOperations(); + + // Auto-fill input when prompt is selected useEffect(() => { - if (inputRef?.current) { - const textarea = inputRef.current; - textarea.style.height = 'auto'; - - // Calculate line height - approximately 1.5em per line - const lineHeight = 24; // Adjust this value based on your CSS line-height - const maxHeight = lineHeight * 8; // 8 lines maximum - - const newHeight = Math.min(textarea.scrollHeight, maxHeight); - textarea.style.height = `${newHeight}px`; - - // Enable/disable scroll based on content height - if (textarea.scrollHeight > maxHeight) { - textarea.style.overflowY = 'auto'; - } else { - textarea.style.overflowY = 'hidden'; - } + if (selectedPrompt) { + setInputValue(selectedPrompt.content); } - }, [inputValue, inputRef]); + }, [selectedPrompt]); - const handleAttachmentClick = () => { - setIsUploadModalOpen(true); + const handleSend = async () => { + if (!inputValue.trim() || startingWorkflow) return; + + try { + const result = await startWorkflow({ + prompt: inputValue, + listFileId: currentAttachedFiles.map(f => f.id) + }); + + if (result.success) { + setInputValue(''); + if (onAttachedFilesChange) { + onAttachedFilesChange([]); + } + if (onPromptUsed) onPromptUsed(); + if (onWorkflowIdChange && result.data?.id) { + onWorkflowIdChange(result.data.id); + } + } + } catch (error) { + console.error('Failed to start workflow:', error); + } }; - const handleFilesSelected = (files: FileInfo[]) => { - onFilesSelect(files); - setIsUploadModalOpen(false); - }; - - const handleFileRemove = (fileId: number) => { - onFileRemove(fileId); - }; - - // Handle Enter key press for sending message (without Shift) - const handleKeyDown = (e: React.KeyboardEvent) => { + const handleKeyPress = (e: React.KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); - if (!isDisabled && (inputValue.trim() || attachedFiles.length > 0)) { - onSend(); - } - } - // Call original onKeyPress if it exists (for backward compatibility) - if (onKeyPress && e.key !== 'Enter') { - onKeyPress(e as any); + handleSend(); } }; - // Drag and drop handlers - const handleDragEnter = (e: React.DragEvent) => { - e.preventDefault(); - e.stopPropagation(); - if (!isDisabled && !isWorkflowRunning) { - setIsDragOver(true); + const handleFilesAttached = (files: AttachedFile[]) => { + setShowFilePopup(false); + if (onAttachedFilesChange) { + onAttachedFilesChange(files); } }; - const handleDragOver = (e: React.DragEvent) => { - e.preventDefault(); - e.stopPropagation(); - }; - - const handleDragLeave = (e: React.DragEvent) => { - e.preventDefault(); - e.stopPropagation(); - // Only set drag over to false if we're leaving the entire input area - if (!e.currentTarget.contains(e.relatedTarget as Node)) { - setIsDragOver(false); - } - }; - - const handleDrop = (e: React.DragEvent) => { - e.preventDefault(); - e.stopPropagation(); - setIsDragOver(false); - - if (isDisabled || isWorkflowRunning) { - return; - } - - const files = Array.from(e.dataTransfer.files); - if (files.length > 0) { - // Convert File objects to FileInfo objects - const fileInfos: FileInfo[] = files.map((file, index) => ({ - id: Date.now() + index, // Generate unique IDs - name: file.name, - mimeType: file.type, - size: file.size, - creationDate: new Date().toISOString(), - source: 'user_uploaded' - })); - - onFilesSelect(fileInfos); - } + const formatFileSize = (bytes: number): string => { + if (bytes < 1024) return bytes + ' B'; + if (bytes < 1024 * 1024) return Math.round(bytes / 1024) + ' KB'; + return Math.round(bytes / (1024 * 1024)) + ' MB'; }; return ( - - {/* Show attached files if any */} - {attachedFiles.length > 0 && ( -
- {attachedFiles.map((file) => ( -
- - {getFileIcon(file.mimeType)} - - - {file.name} - - -
- ))} +
+

Input

+ + {startError && ( +
+ Error: {startError}
)} - - {/* Input row with text input, attachment button, and send button */} -
-