new dashboard
This commit is contained in:
parent
8784373e76
commit
84764f932b
38 changed files with 3462 additions and 1087 deletions
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -26,4 +26,9 @@ dist-ssr
|
|||
# Environment files
|
||||
.env
|
||||
.env.local
|
||||
<<<<<<< Updated upstream
|
||||
.env.*.local
|
||||
=======
|
||||
.env.*.local
|
||||
|
||||
>>>>>>> Stashed changes
|
||||
|
|
|
|||
37
package.json
37
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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
--font-family: "DM Sans", sans-serif;
|
||||
}
|
||||
|
||||
/* Dark theme overrides */
|
||||
.dark-theme {
|
||||
--color-bg: #121212;
|
||||
--color-surface: #1E1E1E;
|
||||
/* 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;
|
||||
}
|
||||
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -48,91 +48,15 @@ const DashboardChat: React.FC<DashboardChatProps> = ({
|
|||
};
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
className={`${styles.dashboard_chat} ${isExpanded ? styles.expanded : ''}`}
|
||||
layout
|
||||
transition={{ duration: 0.4, ease: "easeOut" }}
|
||||
>
|
||||
<motion.div
|
||||
className={styles.chat_header}
|
||||
layout
|
||||
transition={{ duration: 0.3, ease: "easeOut" }}
|
||||
>
|
||||
<div className={styles.chat_button_div}>
|
||||
{[t('dashboard.chat.area'), t('dashboard.chat.history')].map((tab) => (
|
||||
<div key={tab} className={styles.buttonWrapper}>
|
||||
<motion.button
|
||||
key={tab}
|
||||
className={`${styles.chat_button} ${
|
||||
activeTab === tab ? styles.chat_button_active : styles.chat_button_inactive
|
||||
}`}
|
||||
onClick={() => setActiveTab(tab)}
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
>
|
||||
{tab}
|
||||
</motion.button>
|
||||
<AnimatePresence>
|
||||
{activeTab === tab && (
|
||||
<motion.div
|
||||
className={styles.horizontalLine}
|
||||
initial={{ opacity: 0, width: "0%" }}
|
||||
animate={{ opacity: 1, width: "100%" }}
|
||||
exit={{ opacity: 0, width: "0%" }}
|
||||
transition={{ duration: 0.3, ease: "easeOut" }}
|
||||
></motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className={styles.iconContainer}>
|
||||
<motion.div
|
||||
className={styles.expandIcon}
|
||||
onClick={onToggleExpand}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
whileHover={{ scale: 1.15 }}
|
||||
animate={{ rotate: isExpanded ? 180 : 0 }}
|
||||
transition={{ duration: 0.3, ease: "easeOut" }}
|
||||
>
|
||||
{isExpanded ? <BsArrowsAngleContract size={20} /> : <BsArrowsAngleExpand size={20} />}
|
||||
</motion.div>
|
||||
</div>
|
||||
</motion.div>
|
||||
<motion.div
|
||||
className={styles.horizontalLineLight}
|
||||
initial={{ opacity: 0, scaleX: 0 }}
|
||||
animate={{ opacity: 1, scaleX: 1 }}
|
||||
transition={{ duration: 0.3, ease: "easeOut" }}
|
||||
style={{ transformOrigin: "left" }}
|
||||
></motion.div>
|
||||
<motion.div
|
||||
className={styles.chat_content}
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: "auto", opacity: 1 }}
|
||||
transition={{
|
||||
duration: 0.4,
|
||||
ease: [0.4, 0.0, 0.2, 1],
|
||||
height: { duration: 0.4 },
|
||||
opacity: { duration: 0.3 }
|
||||
}}
|
||||
style={{ overflow: "hidden" }}
|
||||
>
|
||||
{activeTab === t('dashboard.chat.area') ? (
|
||||
<DashboardChatArea
|
||||
selectedPrompt={selectedPrompt}
|
||||
onPromptUsed={onPromptUsed}
|
||||
onWorkflowIdChange={onWorkflowIdChange}
|
||||
onWorkflowCompletedChange={onWorkflowCompletedChange}
|
||||
resumeWorkflowId={resumeWorkflowId}
|
||||
/>
|
||||
) : (
|
||||
<DashboardChatHistory
|
||||
onWorkflowResume={handleWorkflowResume}
|
||||
/>
|
||||
)}
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
<div className={styles.dashboard_chat}>
|
||||
<DashboardChatArea
|
||||
selectedPrompt={selectedPrompt}
|
||||
onPromptUsed={onPromptUsed}
|
||||
onWorkflowIdChange={onWorkflowIdChange}
|
||||
onWorkflowCompletedChange={onWorkflowCompletedChange}
|
||||
resumeWorkflowId={resumeWorkflowId}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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<DashboardChatAreaProps> = ({
|
||||
selectedPrompt,
|
||||
|
|
@ -13,90 +13,132 @@ const DashboardChatArea: React.FC<DashboardChatAreaProps> = ({
|
|||
onWorkflowCompletedChange,
|
||||
resumeWorkflowId
|
||||
}) => {
|
||||
const {
|
||||
// State
|
||||
inputValue,
|
||||
setInputValue,
|
||||
currentWorkflowId,
|
||||
workflowCompleted,
|
||||
attachedFiles,
|
||||
// Grid sizing state
|
||||
const [horizontalSplit, setHorizontalSplit] = useState(60); // percentage
|
||||
const [verticalSplit, setVerticalSplit] = useState(60); // percentage
|
||||
const [isDragging, setIsDragging] = useState<'horizontal' | 'vertical' | null>(null);
|
||||
|
||||
// Refs
|
||||
inputRef,
|
||||
messagesEndRef,
|
||||
// File selection state
|
||||
const [selectedFile, setSelectedFile] = useState<any>(null);
|
||||
const [attachedFiles, setAttachedFiles] = useState<any[]>([]);
|
||||
|
||||
// Data from hooks
|
||||
messages,
|
||||
messagesLoading,
|
||||
messagesError,
|
||||
startingWorkflow,
|
||||
startError,
|
||||
workflowStatus,
|
||||
// Workflow state
|
||||
const [currentWorkflowId, setCurrentWorkflowId] = useState<string | null>(resumeWorkflowId || null);
|
||||
|
||||
// Handlers
|
||||
handleSend,
|
||||
handleKeyPress,
|
||||
startNewWorkflow,
|
||||
handleStopWorkflow,
|
||||
handleFileAttach,
|
||||
handleFileRemove,
|
||||
handleFilesSelect,
|
||||
handleRetry,
|
||||
|
||||
// Workflow state
|
||||
isWorkflowRunning,
|
||||
isStoppingWorkflow,
|
||||
shouldShowRetryButton
|
||||
} = useChatLogic({
|
||||
selectedPrompt,
|
||||
onPromptUsed,
|
||||
onWorkflowIdChange,
|
||||
resumeWorkflowId
|
||||
});
|
||||
|
||||
const { t } = useLanguage();
|
||||
|
||||
// Notify parent component when workflow completion status changes
|
||||
useEffect(() => {
|
||||
if (onWorkflowCompletedChange) {
|
||||
onWorkflowCompletedChange(workflowCompleted);
|
||||
// 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 (
|
||||
<div className={styles.chat_area}>
|
||||
<MessageList
|
||||
messages={messages}
|
||||
currentWorkflowId={currentWorkflowId}
|
||||
workflowStatus={workflowStatus}
|
||||
workflowCompleted={workflowCompleted}
|
||||
startingWorkflow={startingWorkflow}
|
||||
startError={startError}
|
||||
messagesError={messagesError}
|
||||
messagesLoading={messagesLoading}
|
||||
onStartNewWorkflow={startNewWorkflow}
|
||||
messagesEndRef={messagesEndRef}
|
||||
handleRetry={handleRetry}
|
||||
shouldShowRetryButton={shouldShowRetryButton}
|
||||
<div
|
||||
className="chat-grid"
|
||||
style={{
|
||||
gridTemplateRows: `${horizontalSplit}% 1px ${100 - horizontalSplit}%`,
|
||||
gridTemplateColumns: `${verticalSplit}% 1px ${100 - verticalSplit}%`
|
||||
}}
|
||||
>
|
||||
{/* Top Left: Message List */}
|
||||
<div className="quadrant messages-quadrant">
|
||||
<MessageList
|
||||
selectedPrompt={selectedPrompt}
|
||||
onPromptUsed={onPromptUsed}
|
||||
resumeWorkflowId={currentWorkflowId}
|
||||
onFilePreview={setSelectedFile}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Vertical Divider */}
|
||||
<div
|
||||
className="divider vertical-divider"
|
||||
onMouseDown={handleMouseDown('vertical')}
|
||||
/>
|
||||
<ChatInput
|
||||
inputValue={inputValue}
|
||||
setInputValue={setInputValue}
|
||||
onSend={handleSend}
|
||||
onKeyPress={handleKeyPress}
|
||||
isDisabled={startingWorkflow}
|
||||
placeholder={placeholder}
|
||||
inputRef={inputRef}
|
||||
isWorkflowRunning={isWorkflowRunning}
|
||||
onStopWorkflow={handleStopWorkflow}
|
||||
isStoppingWorkflow={isStoppingWorkflow}
|
||||
attachedFiles={attachedFiles}
|
||||
onFileAttach={handleFileAttach}
|
||||
onFileRemove={handleFileRemove}
|
||||
onFilesSelect={handleFilesSelect}
|
||||
|
||||
{/* Top Right: File Preview */}
|
||||
<div className="quadrant file-preview-quadrant">
|
||||
<FilePreview selectedFile={selectedFile} />
|
||||
</div>
|
||||
|
||||
{/* Horizontal Divider */}
|
||||
<div
|
||||
className="divider horizontal-divider"
|
||||
onMouseDown={handleMouseDown('horizontal')}
|
||||
/>
|
||||
|
||||
{/* Bottom Left: Input Area */}
|
||||
<div className="quadrant input-quadrant">
|
||||
<InputArea
|
||||
selectedPrompt={selectedPrompt}
|
||||
onPromptUsed={onPromptUsed}
|
||||
onWorkflowIdChange={handleWorkflowIdChange}
|
||||
onAttachedFilesChange={setAttachedFiles}
|
||||
attachedFiles={attachedFiles}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Bottom Right: Connected Files */}
|
||||
<div className="quadrant connected-files-quadrant">
|
||||
<ConnectedFiles
|
||||
onFileSelect={setSelectedFile}
|
||||
selectedFile={selectedFile}
|
||||
attachedFiles={attachedFiles}
|
||||
onRemoveFile={(fileId) => {
|
||||
// If the removed file is currently selected, clear the selection
|
||||
if (selectedFile?.id === fileId) {
|
||||
setSelectedFile(null);
|
||||
}
|
||||
// Remove the file from attached files
|
||||
setAttachedFiles(files => files.filter(f => f.id !== fileId));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,208 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { useFileDownload } from '../../../../hooks/useWorkflows';
|
||||
import { FileInfo } from './dashboardChatAreaTypes';
|
||||
|
||||
interface AttachedFile {
|
||||
id: number;
|
||||
name: string;
|
||||
size: number;
|
||||
type: string;
|
||||
fileData?: File;
|
||||
objectUrl?: string;
|
||||
}
|
||||
|
||||
interface ConnectedFilesProps {
|
||||
onFileSelect?: (file: FileInfo) => void;
|
||||
selectedFile?: FileInfo | null;
|
||||
attachedFiles?: AttachedFile[];
|
||||
onRemoveFile?: (fileId: number) => void;
|
||||
}
|
||||
|
||||
const ConnectedFiles: React.FC<ConnectedFilesProps> = ({
|
||||
onFileSelect,
|
||||
selectedFile,
|
||||
attachedFiles = [],
|
||||
onRemoveFile
|
||||
}) => {
|
||||
const [files, setFiles] = useState<FileInfo[]>([]);
|
||||
const { downloadFile, isDownloading } = useFileDownload();
|
||||
|
||||
// Convert attached files to FileInfo format for compatibility with preview
|
||||
const convertedAttachedFiles = attachedFiles.map(file => {
|
||||
console.log('ConnectedFiles: Converting attached file:', file.name, 'Has fileData:', !!file.fileData, 'Has objectUrl:', !!file.objectUrl);
|
||||
return {
|
||||
id: file.id,
|
||||
name: file.name,
|
||||
mimeType: file.type,
|
||||
size: file.size,
|
||||
creationDate: new Date().toISOString(),
|
||||
fileData: file.fileData,
|
||||
objectUrl: file.objectUrl
|
||||
};
|
||||
});
|
||||
|
||||
// Combine attached files with workflow files
|
||||
const allFiles = [...convertedAttachedFiles, ...files];
|
||||
|
||||
useEffect(() => {
|
||||
// Could load workflow-specific files here in the future
|
||||
}, []);
|
||||
|
||||
const getFileIcon = (mimeType: string) => {
|
||||
if (mimeType.includes('pdf')) return '📄';
|
||||
if (mimeType.includes('spreadsheet') || mimeType.includes('excel')) return '📊';
|
||||
if (mimeType.startsWith('image/')) return '🖼️';
|
||||
if (mimeType.startsWith('text/')) return '📝';
|
||||
return '📎';
|
||||
};
|
||||
|
||||
const formatFileSize = (bytes: number) => {
|
||||
if (bytes < 1024) return bytes + ' B';
|
||||
if (bytes < 1024 * 1024) return Math.round(bytes / 1024) + ' KB';
|
||||
return Math.round(bytes / (1024 * 1024)) + ' MB';
|
||||
};
|
||||
|
||||
const handleFileClick = (file: any) => {
|
||||
if (onFileSelect) {
|
||||
console.log('ConnectedFiles: Selecting file:', file.name, 'Has fileData:', !!file.fileData, 'Has objectUrl:', !!file.objectUrl);
|
||||
onFileSelect(file);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownload = async (file: FileInfo) => {
|
||||
await downloadFile(file.id, file.name);
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: '16px', height: '100%', overflow: 'auto' }}>
|
||||
<h3>Connected Files</h3>
|
||||
|
||||
{/* Show attached files count */}
|
||||
{attachedFiles.length > 0 && (
|
||||
<div style={{
|
||||
marginBottom: '12px',
|
||||
padding: '8px',
|
||||
backgroundColor: '#e3f2fd',
|
||||
borderRadius: '6px',
|
||||
fontSize: '12px',
|
||||
color: '#1976d2',
|
||||
fontWeight: '500'
|
||||
}}>
|
||||
📎 {attachedFiles.length} file{attachedFiles.length !== 1 ? 's' : ''} attached for workflow
|
||||
</div>
|
||||
)}
|
||||
|
||||
{allFiles.length === 0 ? (
|
||||
<p style={{ color: 'var(--color-gray)', textAlign: 'center' }}>
|
||||
No files connected to this workflow
|
||||
</p>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
||||
{allFiles.map((file) => {
|
||||
const isAttachedFile = attachedFiles.some(af => af.id === file.id);
|
||||
return (
|
||||
<div
|
||||
key={file.id}
|
||||
onClick={() => handleFileClick(file)}
|
||||
style={{
|
||||
padding: '12px',
|
||||
border: `1px solid ${selectedFile?.id === file.id ? 'var(--color-secondary)' : 'var(--color-gray-disabled)'}`,
|
||||
borderRadius: '8px',
|
||||
cursor: 'pointer',
|
||||
backgroundColor: selectedFile?.id === file.id ? 'var(--color-secondary-disabled)' : 'var(--color-bg)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '12px',
|
||||
// Highlight attached files
|
||||
...(isAttachedFile && {
|
||||
borderColor: '#1976d2',
|
||||
backgroundColor: '#f3f8ff'
|
||||
})
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: '20px' }}>
|
||||
{getFileIcon(file.mimeType)}
|
||||
</span>
|
||||
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{
|
||||
fontWeight: '500',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '6px'
|
||||
}}>
|
||||
{file.name}
|
||||
{isAttachedFile && (
|
||||
<span style={{
|
||||
fontSize: '10px',
|
||||
backgroundColor: '#1976d2',
|
||||
color: 'white',
|
||||
padding: '2px 6px',
|
||||
borderRadius: '10px',
|
||||
fontWeight: 'normal'
|
||||
}}>
|
||||
ATTACHED
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: '12px',
|
||||
color: 'var(--color-gray)'
|
||||
}}>
|
||||
{file.size ? formatFileSize(file.size) : 'Unknown size'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: '4px' }}>
|
||||
{isAttachedFile && onRemoveFile && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onRemoveFile(file.id);
|
||||
}}
|
||||
style={{
|
||||
padding: '4px 8px',
|
||||
fontSize: '12px',
|
||||
backgroundColor: 'transparent',
|
||||
border: '1px solid #ff6b6b',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
color: '#ff6b6b'
|
||||
}}
|
||||
title="Remove from attachment"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDownload(file);
|
||||
}}
|
||||
disabled={isDownloading}
|
||||
style={{
|
||||
padding: '4px 8px',
|
||||
fontSize: '12px',
|
||||
backgroundColor: 'transparent',
|
||||
border: '1px solid var(--color-gray-disabled)',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
title="Download file"
|
||||
>
|
||||
⬇️
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConnectedFiles;
|
||||
|
|
@ -0,0 +1,192 @@
|
|||
import React from 'react';
|
||||
import { useFilePreview } from '../../../../hooks/useWorkflows';
|
||||
import { FileInfo } from './dashboardChatAreaTypes';
|
||||
|
||||
interface AttachedFileWithData extends FileInfo {
|
||||
fileData?: File;
|
||||
objectUrl?: string;
|
||||
}
|
||||
|
||||
interface FilePreviewProps {
|
||||
selectedFile?: AttachedFileWithData | null;
|
||||
}
|
||||
|
||||
const FilePreview: React.FC<FilePreviewProps> = ({ selectedFile }) => {
|
||||
const { previewContent, fileMetadata, isLoading, error, fetchPreview } = useFilePreview();
|
||||
const [imageUrl, setImageUrl] = React.useState<string | null>(null);
|
||||
|
||||
// Handle base64 image data from backend
|
||||
React.useEffect(() => {
|
||||
if (fileMetadata && fileMetadata.base64Encoded && fileMetadata.preview) {
|
||||
const isImage = fileMetadata.mimeType?.startsWith('image/') ||
|
||||
selectedFile?.name?.toLowerCase().match(/\.(png|jpg|jpeg|gif|bmp|webp)$/);
|
||||
|
||||
if (isImage) {
|
||||
|
||||
const dataUrl = `data:${fileMetadata.mimeType || 'image/png'};base64,${fileMetadata.preview}`;
|
||||
setImageUrl(dataUrl);
|
||||
|
||||
}
|
||||
}
|
||||
}, [fileMetadata, selectedFile]);
|
||||
|
||||
React.useEffect(() => {
|
||||
// Clean up previous object URL
|
||||
const currentImageUrl = imageUrl;
|
||||
if (currentImageUrl && currentImageUrl.startsWith('blob:')) {
|
||||
URL.revokeObjectURL(currentImageUrl);
|
||||
}
|
||||
setImageUrl(null);
|
||||
|
||||
if (selectedFile?.id) {
|
||||
|
||||
// Check if it's an image file (either from mimeType or file extension)
|
||||
const isImage = selectedFile.mimeType?.startsWith('image/') ||
|
||||
selectedFile.name?.toLowerCase().match(/\.(png|jpg|jpeg|gif|bmp|webp)$/);
|
||||
|
||||
if (isImage) {
|
||||
// If it's an attached file with file data, create object URL for preview
|
||||
if (selectedFile.fileData) {
|
||||
const url = URL.createObjectURL(selectedFile.fileData);
|
||||
setImageUrl(url);
|
||||
} else if (selectedFile.objectUrl) {
|
||||
setImageUrl(selectedFile.objectUrl);
|
||||
} else if (selectedFile.downloadUrl) {
|
||||
setImageUrl(selectedFile.downloadUrl);
|
||||
} else {
|
||||
// For existing uploaded files, fetch the image data
|
||||
fetchPreview(selectedFile.id);
|
||||
}
|
||||
} else {
|
||||
// For non-image files, try to fetch preview
|
||||
fetchPreview(selectedFile.id);
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup function
|
||||
return () => {
|
||||
if (currentImageUrl && currentImageUrl.startsWith('blob:')) {
|
||||
URL.revokeObjectURL(currentImageUrl);
|
||||
}
|
||||
};
|
||||
}, [selectedFile?.id, selectedFile?.fileData, selectedFile?.objectUrl]);
|
||||
|
||||
// Cleanup on unmount
|
||||
React.useEffect(() => {
|
||||
return () => {
|
||||
if (imageUrl) {
|
||||
URL.revokeObjectURL(imageUrl);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const getFileType = (mimeType?: string) => {
|
||||
if (!mimeType) return 'Unknown';
|
||||
if (mimeType.startsWith('image/')) return 'Image';
|
||||
if (mimeType.startsWith('text/')) return 'Text';
|
||||
if (mimeType.includes('pdf')) return 'PDF';
|
||||
if (mimeType.includes('spreadsheet') || mimeType.includes('excel')) return 'Spreadsheet';
|
||||
return 'Document';
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: '16px', height: '100%', overflow: 'auto' }}>
|
||||
<h3>File Preview</h3>
|
||||
|
||||
{!selectedFile && (
|
||||
<p style={{ color: 'var(--color-gray)', textAlign: 'center' }}>
|
||||
Select a file to preview
|
||||
</p>
|
||||
)}
|
||||
|
||||
{selectedFile && (
|
||||
<div>
|
||||
<div style={{
|
||||
padding: '12px',
|
||||
backgroundColor: 'var(--color-surface)',
|
||||
borderRadius: '8px',
|
||||
marginBottom: '16px'
|
||||
}}>
|
||||
<h4 style={{ margin: '0 0 8px 0' }}>{selectedFile.name}</h4>
|
||||
<div style={{ fontSize: '12px', color: 'var(--color-gray)' }}>
|
||||
Type: {getFileType(selectedFile.mimeType)} •
|
||||
Size: {selectedFile.size ? Math.round(selectedFile.size / 1024) + ' KB' : 'Unknown'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Image Preview - Show first for images */}
|
||||
{(selectedFile.mimeType?.startsWith('image/') || selectedFile.name?.toLowerCase().match(/\.(png|jpg|jpeg|gif|bmp|webp)$/)) && imageUrl ? (
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<img
|
||||
src={imageUrl}
|
||||
alt={selectedFile.name}
|
||||
style={{
|
||||
maxWidth: '100%',
|
||||
maxHeight: '500px',
|
||||
objectFit: 'contain',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid var(--color-gray-disabled)',
|
||||
display: 'block',
|
||||
margin: '0 auto'
|
||||
}}
|
||||
onLoad={() => console.log('Image loaded successfully')}
|
||||
onError={(e) => {
|
||||
console.error('Image failed to load:', e);
|
||||
console.log('Image URL:', imageUrl);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : (selectedFile.mimeType?.startsWith('image/') || selectedFile.name?.toLowerCase().match(/\.(png|jpg|jpeg|gif|bmp|webp)$/)) ? (
|
||||
<div style={{
|
||||
padding: '20px',
|
||||
textAlign: 'center',
|
||||
backgroundColor: 'var(--color-surface)',
|
||||
borderRadius: '8px',
|
||||
marginBottom: '16px'
|
||||
}}>
|
||||
<p>🖼️ Image preview loading...</p>
|
||||
<small style={{ color: 'var(--color-gray)' }}>
|
||||
Debug: imageUrl={imageUrl ? 'yes' : 'no'}, downloadUrl={selectedFile.downloadUrl ? 'yes' : 'no'},
|
||||
fileData={selectedFile.fileData ? 'yes' : 'no'}, objectUrl={selectedFile.objectUrl ? 'yes' : 'no'}
|
||||
<br />
|
||||
MimeType: {selectedFile.mimeType}
|
||||
</small>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* Text/Code Preview - Only for non-images and when we don't have an image URL */}
|
||||
{!(selectedFile.mimeType?.startsWith('image/') || selectedFile.name?.toLowerCase().match(/\.(png|jpg|jpeg|gif|bmp|webp)$/)) && !imageUrl && (
|
||||
<>
|
||||
{isLoading && <p>Loading preview...</p>}
|
||||
{error && <p style={{ color: 'red' }}>Error: {error}</p>}
|
||||
|
||||
{previewContent && (
|
||||
<div style={{
|
||||
backgroundColor: 'var(--color-surface)',
|
||||
padding: '16px',
|
||||
borderRadius: '8px',
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '12px',
|
||||
lineHeight: '1.4',
|
||||
whiteSpace: 'pre-wrap',
|
||||
overflow: 'auto',
|
||||
maxHeight: '400px'
|
||||
}}>
|
||||
{previewContent}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!previewContent && !isLoading && !error && (
|
||||
<p style={{ color: 'var(--color-gray)' }}>
|
||||
Preview not available for this file type
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FilePreview;
|
||||
|
|
@ -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 '📄';
|
||||
interface InputAreaProps {
|
||||
selectedPrompt?: Prompt | null;
|
||||
onPromptUsed?: () => void;
|
||||
onWorkflowIdChange?: (workflowId: string | null) => void;
|
||||
onAttachedFilesChange?: (files: AttachedFile[]) => void;
|
||||
attachedFiles?: AttachedFile[];
|
||||
}
|
||||
|
||||
const type = mimeType.toLowerCase();
|
||||
interface AttachedFile {
|
||||
id: number;
|
||||
name: string;
|
||||
size: number;
|
||||
type: string;
|
||||
fileData?: File;
|
||||
objectUrl?: string;
|
||||
}
|
||||
|
||||
if (type.includes('image')) return '🖼️';
|
||||
if (type.includes('video')) return '🎥';
|
||||
if (type.includes('audio')) return '🎵';
|
||||
if (type.includes('pdf')) return '📕';
|
||||
if (type.includes('word') || type.includes('document')) return '📘';
|
||||
if (type.includes('excel') || type.includes('spreadsheet')) return '📊';
|
||||
if (type.includes('powerpoint') || type.includes('presentation')) return '📋';
|
||||
if (type.includes('text')) return '📝';
|
||||
if (type.includes('zip') || type.includes('archive')) return '📦';
|
||||
if (type.includes('javascript') || type.includes('json') || type.includes('html') || type.includes('css')) return '💻';
|
||||
|
||||
return '📄';
|
||||
};
|
||||
|
||||
const ChatInput: React.FC<ChatInputProps> = ({
|
||||
inputValue,
|
||||
setInputValue,
|
||||
onSend,
|
||||
onKeyPress,
|
||||
isDisabled,
|
||||
placeholder,
|
||||
inputRef,
|
||||
isWorkflowRunning,
|
||||
onStopWorkflow,
|
||||
isStoppingWorkflow,
|
||||
attachedFiles,
|
||||
onFileAttach,
|
||||
onFileRemove,
|
||||
onFilesSelect
|
||||
const InputArea: React.FC<InputAreaProps> = ({
|
||||
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<HTMLTextAreaElement>) => {
|
||||
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 (
|
||||
<motion.div
|
||||
className={`${styles.chat_input} ${isDragOver ? styles.drag_over : ''}`}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.3, duration: 0.3, ease: "easeOut" }}
|
||||
onDragEnter={handleDragEnter}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
{/* Show attached files if any */}
|
||||
{attachedFiles.length > 0 && (
|
||||
<div className={styles.attached_files}>
|
||||
{attachedFiles.map((file) => (
|
||||
<div key={file.id} className={styles.attached_file}>
|
||||
<span className={styles.attached_file_icon}>
|
||||
{getFileIcon(file.mimeType)}
|
||||
</span>
|
||||
<span className={styles.attached_file_name}>
|
||||
{file.name}
|
||||
</span>
|
||||
<button
|
||||
className={styles.attached_file_remove}
|
||||
onClick={() => handleFileRemove(file.id)}
|
||||
title={t('chat.remove_file')}
|
||||
>
|
||||
<IoClose size={12} />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<div style={{ padding: '16px', height: '100%', display: 'flex', flexDirection: 'column' }}>
|
||||
<h3>Input</h3>
|
||||
|
||||
{startError && (
|
||||
<div style={{
|
||||
padding: '8px',
|
||||
backgroundColor: '#ffe6e6',
|
||||
color: '#d00',
|
||||
borderRadius: '4px',
|
||||
marginBottom: '12px'
|
||||
}}>
|
||||
Error: {startError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Input row with text input, attachment button, and send button */}
|
||||
<div className={styles.input_row}>
|
||||
{/* Show attached files count */}
|
||||
{currentAttachedFiles.length > 0 && (
|
||||
<div style={{
|
||||
marginBottom: '8px',
|
||||
padding: '6px 10px',
|
||||
backgroundColor: '#e3f2fd',
|
||||
borderRadius: '4px',
|
||||
fontSize: '12px',
|
||||
color: '#1976d2',
|
||||
textAlign: 'center'
|
||||
}}>
|
||||
📎 {currentAttachedFiles.length} file{currentAttachedFiles.length !== 1 ? 's' : ''} attached
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px', flex: 1 }}>
|
||||
<textarea
|
||||
ref={inputRef}
|
||||
value={inputValue}
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={placeholder}
|
||||
className={styles.message_input}
|
||||
disabled={isDisabled}
|
||||
rows={1}
|
||||
onKeyPress={handleKeyPress}
|
||||
placeholder="Enter your message or prompt..."
|
||||
disabled={startingWorkflow}
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: '12px',
|
||||
border: '1px solid var(--color-gray-disabled)',
|
||||
borderRadius: '8px',
|
||||
resize: 'none',
|
||||
minHeight: '24px',
|
||||
lineHeight: '24px'
|
||||
fontSize: '14px',
|
||||
fontFamily: 'inherit'
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Attachment button */}
|
||||
<motion.button
|
||||
className={styles.attachment_button}
|
||||
onClick={handleAttachmentClick}
|
||||
disabled={isDisabled || isWorkflowRunning}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
transition={{ duration: 0.2, ease: "easeOut" }}
|
||||
title={t('chat.attach_file')}
|
||||
>
|
||||
<IoAttach size={26} />
|
||||
</motion.button>
|
||||
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
|
||||
<button
|
||||
onClick={() => setShowFilePopup(true)}
|
||||
style={{
|
||||
padding: '8px 12px',
|
||||
backgroundColor: 'var(--color-surface)',
|
||||
border: '1px solid var(--color-gray-disabled)',
|
||||
borderRadius: '6px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '14px'
|
||||
}}
|
||||
>
|
||||
📎 Attach Files
|
||||
</button>
|
||||
|
||||
{/* 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}/>
|
||||
<button
|
||||
onClick={handleSend}
|
||||
disabled={!inputValue.trim() || startingWorkflow}
|
||||
style={{
|
||||
padding: '8px 16px',
|
||||
backgroundColor: startingWorkflow ? 'var(--color-gray-disabled)' : 'var(--color-secondary)',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '6px',
|
||||
cursor: startingWorkflow ? 'not-allowed' : 'pointer'
|
||||
}}
|
||||
>
|
||||
{startingWorkflow ? 'Starting...' : 'Send'}
|
||||
</button>
|
||||
|
||||
{selectedPrompt && (
|
||||
<span style={{ fontSize: '12px', color: 'var(--color-gray)' }}>
|
||||
Using prompt: {selectedPrompt.name}
|
||||
</span>
|
||||
)}
|
||||
</motion.button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Upload Modal */}
|
||||
<DateienSelector
|
||||
isOpen={isUploadModalOpen}
|
||||
onClose={() => setIsUploadModalOpen(false)}
|
||||
onFilesSelected={handleFilesSelected}
|
||||
/>
|
||||
</motion.div>
|
||||
{/* File Attachment Popup */}
|
||||
{showFilePopup && (
|
||||
<FileAttachmentPopup
|
||||
onClose={() => setShowFilePopup(false)}
|
||||
onFilesSelected={handleFilesAttached}
|
||||
currentAttachedFiles={currentAttachedFiles}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatInput;
|
||||
export default InputArea;
|
||||
|
|
|
|||
|
|
@ -1,15 +1,14 @@
|
|||
import React, { useState } from "react";
|
||||
import { FaDownload } from "react-icons/fa";
|
||||
import { MdOutlineRemoveRedEye } from "react-icons/md";
|
||||
import { Message, Document } from "./dashboardChatAreaTypes";
|
||||
import FilePreviewPopup from "./FilePreviewPopup";
|
||||
import { useFileDownload } from "../../../../hooks/useWorkflows";
|
||||
import { useLanguage } from "../../../../contexts/LanguageContext";
|
||||
import styles from './DashboardChatArea.module.css';
|
||||
import { Message, Document } from "./dashboardChatAreaTypes";
|
||||
|
||||
interface MessageItemProps {
|
||||
message: Message;
|
||||
index: number;
|
||||
onFilePreview?: (file: any) => void;
|
||||
}
|
||||
|
||||
// Helper function to format file size
|
||||
|
|
@ -62,15 +61,22 @@ const getFileIcon = (type?: string, ext?: string): string => {
|
|||
return '📄';
|
||||
};
|
||||
|
||||
const MessageItem: React.FC<MessageItemProps> = ({ message, index }) => {
|
||||
const MessageItem: React.FC<MessageItemProps> = ({ message, index, onFilePreview }) => {
|
||||
const { t } = useLanguage();
|
||||
const [previewDocument, setPreviewDocument] = useState<Document | null>(null);
|
||||
const [isPreviewOpen, setIsPreviewOpen] = useState(false);
|
||||
const { downloadFile, isDownloading, error: downloadError } = useFileDownload();
|
||||
|
||||
|
||||
// Debug: Log what the MessageItem is receiving
|
||||
console.log(`🎭 MessageItem rendering:`, {
|
||||
messageId: message.id,
|
||||
messageRole: message.role,
|
||||
hasDocuments: !!(message.documents),
|
||||
documentsArray: message.documents,
|
||||
documentsLength: message.documents?.length || 0,
|
||||
documentsCheck: message.documents && message.documents.length > 0
|
||||
});
|
||||
|
||||
const handleDocumentClick = (document: Document) => {
|
||||
console.log(`🖱️ Document clicked:`, document);
|
||||
// If there's a downloadUrl, use it; otherwise try the url
|
||||
const downloadLink = document.downloadUrl || document.url;
|
||||
|
||||
|
|
@ -83,109 +89,184 @@ const MessageItem: React.FC<MessageItemProps> = ({ message, index }) => {
|
|||
const handlePreview = (document: Document, e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
|
||||
// Use fileId if available, otherwise try to use id as fallback
|
||||
const fileId = document.fileId || document.id;
|
||||
console.log(`👁️ Preview requested for:`, document);
|
||||
|
||||
if (!fileId) {
|
||||
// Use fileId if available, otherwise try to use id as fallback
|
||||
const fileId = document.fileId || parseInt(document.id || '0');
|
||||
|
||||
if (!fileId || isNaN(fileId)) {
|
||||
console.error('❌ Invalid file ID for preview:', document);
|
||||
return;
|
||||
}
|
||||
|
||||
setPreviewDocument(document);
|
||||
setIsPreviewOpen(true);
|
||||
};
|
||||
console.log('✅ MessageItem - Previewing file:', { fileId, document });
|
||||
|
||||
const handleClosePreview = () => {
|
||||
setIsPreviewOpen(false);
|
||||
setPreviewDocument(null);
|
||||
// Call the parent callback to show preview in the file preview quadrant
|
||||
if (onFilePreview) {
|
||||
onFilePreview({
|
||||
id: fileId.toString(),
|
||||
name: document.name,
|
||||
mimeType: document.type || 'application/octet-stream',
|
||||
size: document.size,
|
||||
fileId: fileId
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownload = async (document: Document, e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
|
||||
console.log(`⬇️ Download requested for:`, document);
|
||||
|
||||
// Use fileId if available, otherwise try to use id as fallback
|
||||
const fileId = document.fileId || document.id;
|
||||
const fileId = document.fileId || parseInt(document.id || '0');
|
||||
|
||||
if (!fileId) {
|
||||
console.error('❌ No file ID for download:', document);
|
||||
return;
|
||||
}
|
||||
|
||||
// Construct filename with extension if available
|
||||
const fileName = document.ext ? `${document.name}.${document.ext}` : document.name;
|
||||
|
||||
console.log(`💾 Downloading file ${fileId} as "${fileName}"`);
|
||||
await downloadFile(fileId, fileName);
|
||||
};
|
||||
|
||||
// Debug: Log document check before rendering
|
||||
const hasDocuments = message.documents && message.documents.length > 0;
|
||||
console.log(`🔍 About to check documents:`, {
|
||||
hasDocuments: !!(message.documents),
|
||||
documentsLength: message.documents?.length || 0,
|
||||
willRenderFiles: hasDocuments
|
||||
});
|
||||
|
||||
// Log if no documents
|
||||
if (!hasDocuments) {
|
||||
console.log(`📭 No documents to render for message ${message.id}`);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={message.id || index}
|
||||
className={`${styles.message} ${styles[`message_${message.role}`]}`}
|
||||
style={{
|
||||
padding: '12px',
|
||||
borderRadius: '8px',
|
||||
backgroundColor: message.role === 'user'
|
||||
? 'var(--color-secondary-disabled)'
|
||||
: 'var(--color-surface)',
|
||||
marginBottom: '8px'
|
||||
}}
|
||||
>
|
||||
<div className={styles.message_role}>
|
||||
{message.role === 'user' ? t('chat.you') : message.agentName}
|
||||
<div style={{
|
||||
fontSize: '12px',
|
||||
color: 'var(--color-gray)',
|
||||
marginBottom: '4px',
|
||||
fontWeight: '500'
|
||||
}}>
|
||||
{message.role === 'user' ? 'You' : message.agentName}
|
||||
{message.timestamp && ` • ${new Date(message.timestamp).toLocaleTimeString()}`}
|
||||
</div>
|
||||
<div className={styles.message_content}>
|
||||
|
||||
<div style={{
|
||||
lineHeight: '1.5',
|
||||
whiteSpace: 'pre-wrap'
|
||||
}}>
|
||||
{message.content}
|
||||
</div>
|
||||
|
||||
{message.documents && message.documents.length > 0 && (
|
||||
<div className={styles.message_documents}>
|
||||
{message.documents.map((document, docIndex) => (
|
||||
<div
|
||||
key={document.id || docIndex}
|
||||
className={styles.document_item}
|
||||
onClick={() => handleDocumentClick(document)}
|
||||
title={`${t('chat.click_to_open')} ${document.name}`}
|
||||
>
|
||||
<span className={styles.document_icon}>
|
||||
{getFileIcon(document.type, document.ext)}
|
||||
</span>
|
||||
<div className={styles.document_info}>
|
||||
<div className={styles.document_name}>
|
||||
{document.ext ? `${document.name}.${document.ext}` : document.name}
|
||||
</div>
|
||||
<div className={styles.document_meta}>
|
||||
{hasDocuments && (
|
||||
<div style={{
|
||||
marginTop: '12px',
|
||||
padding: '8px',
|
||||
backgroundColor: 'var(--color-bg)',
|
||||
borderRadius: '6px',
|
||||
border: '1px solid var(--color-gray-disabled)'
|
||||
}}>
|
||||
<div style={{
|
||||
fontSize: '12px',
|
||||
color: 'var(--color-gray)',
|
||||
marginBottom: '8px',
|
||||
fontWeight: '500'
|
||||
}}>
|
||||
📎 Attached Files ({message.documents!.length})
|
||||
</div>
|
||||
<div>
|
||||
{message.documents!.map((document, docIndex) => {
|
||||
console.log(`📄 Rendering document ${docIndex + 1}:`, document);
|
||||
return (
|
||||
<div
|
||||
key={document.id || docIndex}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
padding: '6px',
|
||||
borderRadius: '4px',
|
||||
backgroundColor: 'var(--color-surface)',
|
||||
marginBottom: '4px',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
onClick={() => handleDocumentClick(document)}
|
||||
title={`Click to open ${document.name}`}
|
||||
>
|
||||
<span style={{ fontSize: '16px' }}>
|
||||
{getFileIcon(document.type, document.ext)}
|
||||
</span>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{
|
||||
fontSize: '14px',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap'
|
||||
}}>
|
||||
{document.ext ? `${document.name}.${document.ext}` : document.name}
|
||||
</div>
|
||||
{document.size && (
|
||||
<span className={styles.document_size}>
|
||||
<div style={{
|
||||
fontSize: '12px',
|
||||
color: 'var(--color-gray)'
|
||||
}}>
|
||||
{formatFileSize(document.size)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '4px' }}>
|
||||
<button
|
||||
onClick={(e) => handlePreview(document, e)}
|
||||
style={{
|
||||
padding: '4px 8px',
|
||||
fontSize: '12px',
|
||||
backgroundColor: 'transparent',
|
||||
border: '1px solid var(--color-gray-disabled)',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
title="Preview file"
|
||||
>
|
||||
👁️
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => handleDownload(document, e)}
|
||||
disabled={isDownloading}
|
||||
style={{
|
||||
padding: '4px 8px',
|
||||
fontSize: '12px',
|
||||
backgroundColor: 'transparent',
|
||||
border: '1px solid var(--color-gray-disabled)',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
title="Download file"
|
||||
>
|
||||
⬇️
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.document_actions}>
|
||||
<button
|
||||
className={styles.document_action_button}
|
||||
onClick={(e) => handlePreview(document, e)}
|
||||
title={t('chat.preview_document')}
|
||||
>
|
||||
<MdOutlineRemoveRedEye />
|
||||
</button>
|
||||
<button
|
||||
className={styles.document_action_button}
|
||||
onClick={(e) => handleDownload(document, e)}
|
||||
title={t('chat.download_document')}
|
||||
>
|
||||
<FaDownload />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{message.timestamp && (
|
||||
<div className={styles.message_timestamp}>
|
||||
{new Date(message.timestamp).toLocaleTimeString()}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* File Preview Popup */}
|
||||
{previewDocument && (
|
||||
<FilePreviewPopup
|
||||
document={previewDocument}
|
||||
isOpen={isPreviewOpen}
|
||||
onClose={handleClosePreview}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,82 +1,254 @@
|
|||
import React from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { MessageListProps } from "./dashboardChatAreaTypes";
|
||||
import MessageItem from "./DashboardChatAreaMessageItem";
|
||||
import WorkflowStatusDisplay from "./DashbaordChatAreaStatusDisplay";
|
||||
import { useLanguage } from "../../../../contexts/LanguageContext";
|
||||
import styles from './DashboardChatArea.module.css';
|
||||
import React, { useEffect, useRef, useState, useCallback } from 'react';
|
||||
import { useWorkflowStatus } from '../../../../hooks/useWorkflows';
|
||||
import { Prompt } from '../../../../hooks/usePrompts';
|
||||
import { useApiRequest } from '../../../../hooks/useApi';
|
||||
import MessageItem from './DashboardChatAreaMessageItem';
|
||||
import { Message, Document, WorkflowMessage } from './dashboardChatAreaTypes';
|
||||
|
||||
interface MessageListProps {
|
||||
selectedPrompt?: Prompt | null;
|
||||
onPromptUsed?: () => void;
|
||||
resumeWorkflowId?: string | null;
|
||||
onFilePreview?: (file: any) => void;
|
||||
}
|
||||
|
||||
// Custom hook to fetch and transform messages like the old code
|
||||
const useTransformedMessages = (workflowId: string | null) => {
|
||||
const [messages, setMessages] = useState<Message[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const { request } = useApiRequest();
|
||||
|
||||
const fetchMessages = useCallback(async () => {
|
||||
if (!workflowId) {
|
||||
setMessages([]);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
console.log(`🔍 Fetching messages for workflow: ${workflowId}`);
|
||||
|
||||
// Fetch workflow messages
|
||||
const workflowMessages: WorkflowMessage[] = await request({
|
||||
url: `/api/workflows/${workflowId}/messages`,
|
||||
method: 'get'
|
||||
});
|
||||
|
||||
console.log(`📨 Received ${workflowMessages.length} messages from API:`, workflowMessages);
|
||||
|
||||
// Debug each message structure
|
||||
workflowMessages.forEach((msg, index) => {
|
||||
console.log(`📄 Message ${index + 1}:`, {
|
||||
id: msg.id,
|
||||
role: msg.role,
|
||||
content: msg.content?.substring(0, 50) + '...',
|
||||
fileIds: msg.fileIds,
|
||||
hasFileIds: !!(msg.fileIds && msg.fileIds.length > 0),
|
||||
fileIdsLength: msg.fileIds?.length || 0
|
||||
});
|
||||
});
|
||||
|
||||
// Transform each message
|
||||
const transformedMessages = await Promise.all(
|
||||
workflowMessages.map(async (workflowMessage: WorkflowMessage, msgIndex) => {
|
||||
console.log(`🔄 Transforming message ${msgIndex + 1} (${workflowMessage.id})`);
|
||||
let documents: Document[] = [];
|
||||
|
||||
// Fetch file metadata if fileIds exist
|
||||
if (workflowMessage.fileIds && workflowMessage.fileIds.length > 0) {
|
||||
console.log(`📎 Message ${workflowMessage.id} has ${workflowMessage.fileIds.length} fileIds:`, workflowMessage.fileIds);
|
||||
|
||||
const documentPromises = workflowMessage.fileIds.map(async (fileId, fileIndex) => {
|
||||
try {
|
||||
console.log(`📁 Fetching metadata for file ${fileIndex + 1}/${workflowMessage.fileIds!.length}: ${fileId}`);
|
||||
const response = await request({
|
||||
url: `/api/workflows/files/${fileId}/preview`,
|
||||
method: 'get'
|
||||
});
|
||||
|
||||
console.log(`✅ File ${fileId} metadata received:`, response);
|
||||
|
||||
const document: Document = {
|
||||
id: fileId.toString(),
|
||||
fileId: fileId,
|
||||
name: response.name || response.fileName || `File_${fileId}`,
|
||||
ext: response.extension || response.ext || (response.name ? response.name.split('.').pop() : 'txt'),
|
||||
type: response.mimeType || response.type || 'application/octet-stream',
|
||||
size: response.size || 0,
|
||||
downloadUrl: response.downloadUrl || response.url
|
||||
};
|
||||
|
||||
console.log(`🗂️ Created document object:`, document);
|
||||
return document;
|
||||
} catch (error) {
|
||||
console.error(`❌ Failed to fetch metadata for file ${fileId}:`, error);
|
||||
// Return a fallback object for failed requests
|
||||
const fallbackDoc: Document = {
|
||||
id: fileId.toString(),
|
||||
fileId: fileId,
|
||||
name: `File_${fileId}`,
|
||||
ext: 'unknown',
|
||||
type: 'application/octet-stream',
|
||||
size: 0
|
||||
};
|
||||
console.log(`🔧 Created fallback document:`, fallbackDoc);
|
||||
return fallbackDoc;
|
||||
}
|
||||
});
|
||||
|
||||
documents = await Promise.all(documentPromises);
|
||||
console.log(`📋 All files processed for message ${workflowMessage.id}. Total documents: ${documents.length}`);
|
||||
} else {
|
||||
console.log(`📭 Message ${workflowMessage.id} has no fileIds`);
|
||||
}
|
||||
|
||||
// Transform to old Message format
|
||||
const message: Message = {
|
||||
id: workflowMessage.id,
|
||||
role: workflowMessage.role,
|
||||
agentName: workflowMessage.role === 'user' ? 'You' : 'Assistant',
|
||||
content: workflowMessage.content,
|
||||
timestamp: workflowMessage.timestamp,
|
||||
documents: documents
|
||||
};
|
||||
|
||||
console.log(`✨ Final transformed message:`, {
|
||||
id: message.id,
|
||||
role: message.role,
|
||||
documentsCount: message.documents?.length || 0,
|
||||
hasDocuments: !!(message.documents && message.documents.length > 0)
|
||||
});
|
||||
|
||||
return message;
|
||||
})
|
||||
);
|
||||
|
||||
console.log(`🎉 Successfully transformed all ${transformedMessages.length} messages`);
|
||||
console.log(`📊 Summary:`, transformedMessages.map(msg => ({
|
||||
id: msg.id,
|
||||
role: msg.role,
|
||||
documentsCount: msg.documents?.length || 0
|
||||
})));
|
||||
|
||||
setMessages(transformedMessages);
|
||||
} catch (err: any) {
|
||||
console.error('💥 Error fetching messages:', err);
|
||||
setError(err.message || 'Failed to fetch messages');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [workflowId, request]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchMessages();
|
||||
}, [fetchMessages]);
|
||||
|
||||
return { messages, loading, error, refetch: fetchMessages };
|
||||
};
|
||||
|
||||
const MessageList: React.FC<MessageListProps> = ({
|
||||
messages,
|
||||
currentWorkflowId,
|
||||
workflowStatus,
|
||||
workflowCompleted,
|
||||
startingWorkflow,
|
||||
startError,
|
||||
messagesError,
|
||||
messagesLoading,
|
||||
onStartNewWorkflow,
|
||||
messagesEndRef,
|
||||
handleRetry,
|
||||
shouldShowRetryButton
|
||||
selectedPrompt,
|
||||
onPromptUsed,
|
||||
resumeWorkflowId,
|
||||
onFilePreview
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
const { messages, loading, error, refetch } = useTransformedMessages(resumeWorkflowId || null);
|
||||
const { status } = useWorkflowStatus(resumeWorkflowId || null);
|
||||
const intervalRef = useRef<number | null>(null);
|
||||
const [isInitialLoad, setIsInitialLoad] = React.useState(true);
|
||||
|
||||
// Auto-refresh messages every 3 seconds when workflow is active
|
||||
useEffect(() => {
|
||||
if (resumeWorkflowId && status?.status &&
|
||||
(status.status === 'running' || status.status === 'processing' || status.status === 'started')) {
|
||||
intervalRef.current = window.setInterval(() => {
|
||||
console.log('🔄 Auto-refreshing messages due to active workflow');
|
||||
refetch();
|
||||
}, 3000);
|
||||
} else {
|
||||
// Stop polling when completed, failed, or no workflow
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current);
|
||||
intervalRef.current = null;
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current);
|
||||
}
|
||||
};
|
||||
}, [resumeWorkflowId, status?.status, refetch]);
|
||||
|
||||
// Initial load when workflow ID changes
|
||||
useEffect(() => {
|
||||
if (resumeWorkflowId) {
|
||||
console.log(`🚀 Starting initial load for workflow: ${resumeWorkflowId}`);
|
||||
setIsInitialLoad(true);
|
||||
refetch().finally(() => setIsInitialLoad(false));
|
||||
} else {
|
||||
setIsInitialLoad(false);
|
||||
}
|
||||
}, [resumeWorkflowId, refetch]);
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
className={styles.chat_messages}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.2, duration: 0.3, ease: "easeOut" }}
|
||||
>
|
||||
<div className={styles.messages_container}>
|
||||
{startingWorkflow && (
|
||||
<div className={styles.loading_message}>
|
||||
<p>{workflowCompleted && currentWorkflowId ? t('chat.sending_followup', 'Sending follow-up message...') : t('chat.sending_message', 'Sending message...')}</p>
|
||||
</div>
|
||||
)}
|
||||
{startError && (
|
||||
<div className={styles.error_message}>
|
||||
<p>{t('chat.error_prefix', 'Error:')} {startError}</p>
|
||||
</div>
|
||||
)}
|
||||
{messagesError && (
|
||||
<div className={styles.error_message}>
|
||||
<p>{t('chat.error_loading_messages', 'Error loading messages:')} {messagesError}</p>
|
||||
</div>
|
||||
)}
|
||||
{currentWorkflowId && messagesLoading && messages.length === 0 && (
|
||||
<div className={styles.loading_message}>
|
||||
<p>{t('chat.loading_workflow_messages', 'Loading workflow messages...')}</p>
|
||||
</div>
|
||||
)}
|
||||
{messages.length > 0 ? (
|
||||
messages.map((message, index) => (
|
||||
<div style={{ padding: '16px', height: '100%', overflow: 'auto' }}>
|
||||
<h3>Messages</h3>
|
||||
|
||||
{error && <p style={{ color: 'red' }}>Error: {error}</p>}
|
||||
|
||||
{status && (
|
||||
<div style={{
|
||||
padding: '8px',
|
||||
backgroundColor: 'var(--color-surface)',
|
||||
borderRadius: '4px',
|
||||
marginBottom: '16px'
|
||||
}}>
|
||||
<strong>Status:</strong> {status.status}
|
||||
{status.currentRound && ` (Round ${status.currentRound})`}
|
||||
{/* Show a small indicator when polling for updates */}
|
||||
{intervalRef.current && (
|
||||
<span style={{
|
||||
marginLeft: '8px',
|
||||
fontSize: '12px',
|
||||
color: 'var(--color-secondary)',
|
||||
opacity: 0.7
|
||||
}}>
|
||||
🔄 Live updates
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
{messages.map((message, index) => {
|
||||
console.log(`🎨 Rendering message ${message.id} with ${message.documents?.length || 0} documents`);
|
||||
return (
|
||||
<MessageItem
|
||||
key={message.id || index}
|
||||
key={message.id}
|
||||
message={message}
|
||||
index={index}
|
||||
onFilePreview={onFilePreview}
|
||||
/>
|
||||
))
|
||||
) : !currentWorkflowId ? (
|
||||
<p className={styles.placeholder_text}>{t('chat.start_conversation', 'Start a conversation by entering a message, selecting a template, or continuing a previous workflow...')}</p>
|
||||
) : null}
|
||||
|
||||
{/* Spacer to push workflow status to bottom when there are fewer messages */}
|
||||
{messages.length < 3 && <div className={styles.messages_spacer} />}
|
||||
|
||||
<WorkflowStatusDisplay
|
||||
currentWorkflowId={currentWorkflowId}
|
||||
workflowStatus={workflowStatus}
|
||||
workflowCompleted={workflowCompleted}
|
||||
onStartNewWorkflow={onStartNewWorkflow}
|
||||
handleRetry={handleRetry}
|
||||
shouldShowRetryButton={shouldShowRetryButton}
|
||||
/>
|
||||
|
||||
<div ref={messagesEndRef} />
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{loading && (
|
||||
<div style={{ textAlign: 'center', padding: '16px' }}>
|
||||
<p style={{ color: 'var(--color-gray)' }}>Loading messages...</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{messages.length === 0 && !isInitialLoad && !loading && (
|
||||
<p style={{ color: 'var(--color-gray)', textAlign: 'center' }}>
|
||||
No messages yet. Start a workflow to see messages here.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,104 @@
|
|||
import React, { useEffect } from "react";
|
||||
import { DashboardChatAreaProps } from "./dashboardChatAreaTypes";
|
||||
import { useChatLogic } from "./dashboardChatAreaLogic";
|
||||
import { useLanguage } from "../../../../contexts/LanguageContext";
|
||||
import MessageList from "./DashboardChatAreaMessageList";
|
||||
import ChatInput from "./DashboardChatAreaInput";
|
||||
import styles from './DashboardChatArea.module.css';
|
||||
|
||||
const DashboardChatArea: React.FC<DashboardChatAreaProps> = ({
|
||||
selectedPrompt,
|
||||
onPromptUsed,
|
||||
onWorkflowIdChange,
|
||||
onWorkflowCompletedChange,
|
||||
resumeWorkflowId
|
||||
}) => {
|
||||
const {
|
||||
// State
|
||||
inputValue,
|
||||
setInputValue,
|
||||
currentWorkflowId,
|
||||
workflowCompleted,
|
||||
attachedFiles,
|
||||
|
||||
// Refs
|
||||
inputRef,
|
||||
messagesEndRef,
|
||||
|
||||
// Data from hooks
|
||||
messages,
|
||||
messagesLoading,
|
||||
messagesError,
|
||||
startingWorkflow,
|
||||
startError,
|
||||
workflowStatus,
|
||||
|
||||
// Handlers
|
||||
handleSend,
|
||||
handleKeyPress,
|
||||
startNewWorkflow,
|
||||
handleStopWorkflow,
|
||||
handleFileAttach,
|
||||
handleFileRemove,
|
||||
handleFilesSelect,
|
||||
handleRetry,
|
||||
|
||||
// Workflow state
|
||||
isWorkflowRunning,
|
||||
isStoppingWorkflow,
|
||||
shouldShowRetryButton
|
||||
} = useChatLogic({
|
||||
selectedPrompt,
|
||||
onPromptUsed,
|
||||
onWorkflowIdChange,
|
||||
resumeWorkflowId
|
||||
});
|
||||
|
||||
const { t } = useLanguage();
|
||||
|
||||
// Notify parent component when workflow completion status changes
|
||||
useEffect(() => {
|
||||
if (onWorkflowCompletedChange) {
|
||||
onWorkflowCompletedChange(workflowCompleted);
|
||||
}
|
||||
}, [workflowCompleted, onWorkflowCompletedChange]);
|
||||
|
||||
const placeholder = workflowCompleted ? t('chat.continue_conversation') : t('chat.enter_message');
|
||||
|
||||
return (
|
||||
<div className={styles.chat_area}>
|
||||
<MessageList
|
||||
messages={messages}
|
||||
currentWorkflowId={currentWorkflowId}
|
||||
workflowStatus={workflowStatus}
|
||||
workflowCompleted={workflowCompleted}
|
||||
startingWorkflow={startingWorkflow}
|
||||
startError={startError}
|
||||
messagesError={messagesError}
|
||||
messagesLoading={messagesLoading}
|
||||
onStartNewWorkflow={startNewWorkflow}
|
||||
messagesEndRef={messagesEndRef}
|
||||
handleRetry={handleRetry}
|
||||
shouldShowRetryButton={shouldShowRetryButton}
|
||||
/>
|
||||
<ChatInput
|
||||
inputValue={inputValue}
|
||||
setInputValue={setInputValue}
|
||||
onSend={handleSend}
|
||||
onKeyPress={handleKeyPress}
|
||||
isDisabled={startingWorkflow}
|
||||
placeholder={placeholder}
|
||||
inputRef={inputRef}
|
||||
isWorkflowRunning={isWorkflowRunning}
|
||||
onStopWorkflow={handleStopWorkflow}
|
||||
isStoppingWorkflow={isStoppingWorkflow}
|
||||
attachedFiles={attachedFiles}
|
||||
onFileAttach={handleFileAttach}
|
||||
onFileRemove={handleFileRemove}
|
||||
onFilesSelect={handleFilesSelect}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardChatArea;
|
||||
|
|
@ -0,0 +1,239 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { LuSendHorizontal } from "react-icons/lu";
|
||||
import { FaStop } from "react-icons/fa";
|
||||
import { IoAttach, IoClose } from "react-icons/io5";
|
||||
import { ChatInputProps } from "./dashboardChatAreaTypes";
|
||||
import { FileInfo } from "../../../../hooks/useFiles";
|
||||
import { useLanguage } from "../../../../contexts/LanguageContext";
|
||||
import DateienSelector from "../../../Dateien/DateienHinzufügen/DateienSelector";
|
||||
import styles from './DashboardChatArea.module.css';
|
||||
|
||||
// Helper function to get file icon based on type
|
||||
const getFileIcon = (mimeType?: string): string => {
|
||||
if (!mimeType) return '📄';
|
||||
|
||||
const type = mimeType.toLowerCase();
|
||||
|
||||
if (type.includes('image')) return '🖼️';
|
||||
if (type.includes('video')) return '🎥';
|
||||
if (type.includes('audio')) return '🎵';
|
||||
if (type.includes('pdf')) return '📕';
|
||||
if (type.includes('word') || type.includes('document')) return '📘';
|
||||
if (type.includes('excel') || type.includes('spreadsheet')) return '📊';
|
||||
if (type.includes('powerpoint') || type.includes('presentation')) return '📋';
|
||||
if (type.includes('text')) return '📝';
|
||||
if (type.includes('zip') || type.includes('archive')) return '📦';
|
||||
if (type.includes('javascript') || type.includes('json') || type.includes('html') || type.includes('css')) return '💻';
|
||||
|
||||
return '📄';
|
||||
};
|
||||
|
||||
const ChatInput: React.FC<ChatInputProps> = ({
|
||||
inputValue,
|
||||
setInputValue,
|
||||
onSend,
|
||||
onKeyPress,
|
||||
isDisabled,
|
||||
placeholder,
|
||||
inputRef,
|
||||
isWorkflowRunning,
|
||||
onStopWorkflow,
|
||||
isStoppingWorkflow,
|
||||
attachedFiles,
|
||||
onFileAttach,
|
||||
onFileRemove,
|
||||
onFilesSelect
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
const [isUploadModalOpen, setIsUploadModalOpen] = useState(false);
|
||||
const [isDragOver, setIsDragOver] = useState(false);
|
||||
|
||||
// Auto-resize textarea functionality
|
||||
useEffect(() => {
|
||||
if (inputRef?.current) {
|
||||
const textarea = inputRef.current;
|
||||
textarea.style.height = 'auto';
|
||||
|
||||
// Calculate line height - approximately 1.5em per line
|
||||
const lineHeight = 24; // Adjust this value based on your CSS line-height
|
||||
const maxHeight = lineHeight * 8; // 8 lines maximum
|
||||
|
||||
const newHeight = Math.min(textarea.scrollHeight, maxHeight);
|
||||
textarea.style.height = `${newHeight}px`;
|
||||
|
||||
// Enable/disable scroll based on content height
|
||||
if (textarea.scrollHeight > maxHeight) {
|
||||
textarea.style.overflowY = 'auto';
|
||||
} else {
|
||||
textarea.style.overflowY = 'hidden';
|
||||
}
|
||||
}
|
||||
}, [inputValue, inputRef]);
|
||||
|
||||
const handleAttachmentClick = () => {
|
||||
setIsUploadModalOpen(true);
|
||||
};
|
||||
|
||||
const handleFilesSelected = (files: FileInfo[]) => {
|
||||
onFilesSelect(files);
|
||||
setIsUploadModalOpen(false);
|
||||
};
|
||||
|
||||
const handleFileRemove = (fileId: number) => {
|
||||
onFileRemove(fileId);
|
||||
};
|
||||
|
||||
// Handle Enter key press for sending message (without Shift)
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
if (!isDisabled && (inputValue.trim() || attachedFiles.length > 0)) {
|
||||
onSend();
|
||||
}
|
||||
}
|
||||
// Call original onKeyPress if it exists (for backward compatibility)
|
||||
if (onKeyPress && e.key !== 'Enter') {
|
||||
onKeyPress(e as any);
|
||||
}
|
||||
};
|
||||
|
||||
// Drag and drop handlers
|
||||
const handleDragEnter = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (!isDisabled && !isWorkflowRunning) {
|
||||
setIsDragOver(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDragOver = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
const handleDragLeave = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
// Only set drag over to false if we're leaving the entire input area
|
||||
if (!e.currentTarget.contains(e.relatedTarget as Node)) {
|
||||
setIsDragOver(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDrop = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsDragOver(false);
|
||||
|
||||
if (isDisabled || isWorkflowRunning) {
|
||||
return;
|
||||
}
|
||||
|
||||
const files = Array.from(e.dataTransfer.files);
|
||||
if (files.length > 0) {
|
||||
// Convert File objects to FileInfo objects
|
||||
const fileInfos: FileInfo[] = files.map((file, index) => ({
|
||||
id: Date.now() + index, // Generate unique IDs
|
||||
name: file.name,
|
||||
mimeType: file.type,
|
||||
size: file.size,
|
||||
creationDate: new Date().toISOString(),
|
||||
source: 'user_uploaded'
|
||||
}));
|
||||
|
||||
onFilesSelect(fileInfos);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
className={`${styles.chat_input} ${isDragOver ? styles.drag_over : ''}`}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.3, duration: 0.3, ease: "easeOut" }}
|
||||
onDragEnter={handleDragEnter}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
{/* Show attached files if any */}
|
||||
{attachedFiles.length > 0 && (
|
||||
<div className={styles.attached_files}>
|
||||
{attachedFiles.map((file) => (
|
||||
<div key={file.id} className={styles.attached_file}>
|
||||
<span className={styles.attached_file_icon}>
|
||||
{getFileIcon(file.mimeType)}
|
||||
</span>
|
||||
<span className={styles.attached_file_name}>
|
||||
{file.name}
|
||||
</span>
|
||||
<button
|
||||
className={styles.attached_file_remove}
|
||||
onClick={() => handleFileRemove(file.id)}
|
||||
title={t('chat.remove_file')}
|
||||
>
|
||||
<IoClose size={12} />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Input row with text input, attachment button, and send button */}
|
||||
<div className={styles.input_row}>
|
||||
<textarea
|
||||
ref={inputRef}
|
||||
value={inputValue}
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={placeholder}
|
||||
className={styles.message_input}
|
||||
disabled={isDisabled}
|
||||
rows={1}
|
||||
style={{
|
||||
resize: 'none',
|
||||
minHeight: '24px',
|
||||
lineHeight: '24px'
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Attachment button */}
|
||||
<motion.button
|
||||
className={styles.attachment_button}
|
||||
onClick={handleAttachmentClick}
|
||||
disabled={isDisabled || isWorkflowRunning}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
transition={{ duration: 0.2, ease: "easeOut" }}
|
||||
title={t('chat.attach_file')}
|
||||
>
|
||||
<IoAttach size={26} />
|
||||
</motion.button>
|
||||
|
||||
{/* Send/Stop button */}
|
||||
<motion.button
|
||||
className={isWorkflowRunning ? styles.stop_button : styles.send_button}
|
||||
onClick={isWorkflowRunning ? onStopWorkflow : onSend}
|
||||
disabled={isWorkflowRunning ? isStoppingWorkflow : (isDisabled || (!inputValue.trim() && attachedFiles.length === 0))}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
transition={{ duration: 0.2, ease: "easeOut" }}
|
||||
>
|
||||
{isWorkflowRunning ? (
|
||||
<FaStop className={styles.send_button_icon}/>
|
||||
) : (
|
||||
<LuSendHorizontal className={styles.send_button_icon}/>
|
||||
)}
|
||||
</motion.button>
|
||||
</div>
|
||||
|
||||
{/* Upload Modal */}
|
||||
<DateienSelector
|
||||
isOpen={isUploadModalOpen}
|
||||
onClose={() => setIsUploadModalOpen(false)}
|
||||
onFilesSelected={handleFilesSelected}
|
||||
/>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatInput;
|
||||
|
|
@ -0,0 +1,193 @@
|
|||
import React, { useState } from "react";
|
||||
import { FaDownload } from "react-icons/fa";
|
||||
import { MdOutlineRemoveRedEye } from "react-icons/md";
|
||||
import { Message, Document } from "./dashboardChatAreaTypes";
|
||||
import FilePreviewPopup from "./FilePreviewPopup";
|
||||
import { useFileDownload } from "../../../../hooks/useWorkflows";
|
||||
import { useLanguage } from "../../../../contexts/LanguageContext";
|
||||
import styles from './DashboardChatArea.module.css';
|
||||
|
||||
interface MessageItemProps {
|
||||
message: Message;
|
||||
index: number;
|
||||
}
|
||||
|
||||
// Helper function to format file size
|
||||
const formatFileSize = (bytes?: number): string => {
|
||||
if (!bytes) return '';
|
||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||
return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
|
||||
};
|
||||
|
||||
// Helper function to get file icon based on type or extension
|
||||
const getFileIcon = (type?: string, ext?: string): string => {
|
||||
// Use extension first if available, then fall back to MIME type
|
||||
const extension = ext?.toLowerCase();
|
||||
const mimeType = type?.toLowerCase();
|
||||
|
||||
// Check extension first
|
||||
if (extension) {
|
||||
// Images
|
||||
if (['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'].includes(extension)) return '🖼️';
|
||||
// Videos
|
||||
if (['mp4', 'avi', 'mov', 'wmv', 'flv', 'webm', 'mkv'].includes(extension)) return '🎥';
|
||||
// Audio
|
||||
if (['mp3', 'wav', 'aac', 'flac', 'ogg', 'wma'].includes(extension)) return '🎵';
|
||||
// Documents
|
||||
if (extension === 'pdf') return '📕';
|
||||
if (['doc', 'docx'].includes(extension)) return '📘';
|
||||
if (['xls', 'xlsx'].includes(extension)) return '📊';
|
||||
if (['ppt', 'pptx'].includes(extension)) return '📋';
|
||||
if (['txt', 'md', 'rtf'].includes(extension)) return '📝';
|
||||
// Archives
|
||||
if (['zip', 'rar', '7z', 'tar', 'gz'].includes(extension)) return '📦';
|
||||
// Code files
|
||||
if (['js', 'ts', 'jsx', 'tsx', 'html', 'css', 'py', 'java', 'cpp', 'c', 'php'].includes(extension)) return '💻';
|
||||
}
|
||||
|
||||
// Fall back to MIME type if extension didn't match
|
||||
if (mimeType) {
|
||||
if (mimeType.includes('image')) return '🖼️';
|
||||
if (mimeType.includes('video')) return '🎥';
|
||||
if (mimeType.includes('audio')) return '🎵';
|
||||
if (mimeType.includes('pdf')) return '📕';
|
||||
if (mimeType.includes('text')) return '📝';
|
||||
if (mimeType.includes('word') || mimeType.includes('document')) return '📘';
|
||||
if (mimeType.includes('excel') || mimeType.includes('spreadsheet')) return '📊';
|
||||
if (mimeType.includes('powerpoint') || mimeType.includes('presentation')) return '📋';
|
||||
if (mimeType.includes('zip') || mimeType.includes('archive')) return '📦';
|
||||
}
|
||||
|
||||
return '📄';
|
||||
};
|
||||
|
||||
const MessageItem: React.FC<MessageItemProps> = ({ message, index }) => {
|
||||
const { t } = useLanguage();
|
||||
const [previewDocument, setPreviewDocument] = useState<Document | null>(null);
|
||||
const [isPreviewOpen, setIsPreviewOpen] = useState(false);
|
||||
const { downloadFile, isDownloading, error: downloadError } = useFileDownload();
|
||||
|
||||
|
||||
|
||||
const handleDocumentClick = (document: Document) => {
|
||||
// If there's a downloadUrl, use it; otherwise try the url
|
||||
const downloadLink = document.downloadUrl || document.url;
|
||||
|
||||
if (downloadLink) {
|
||||
// Open the document in a new tab
|
||||
window.open(downloadLink, '_blank');
|
||||
}
|
||||
};
|
||||
|
||||
const handlePreview = (document: Document, e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
|
||||
// Use fileId if available, otherwise try to use id as fallback
|
||||
const fileId = document.fileId || document.id;
|
||||
|
||||
if (!fileId) {
|
||||
return;
|
||||
}
|
||||
|
||||
setPreviewDocument(document);
|
||||
setIsPreviewOpen(true);
|
||||
};
|
||||
|
||||
const handleClosePreview = () => {
|
||||
setIsPreviewOpen(false);
|
||||
setPreviewDocument(null);
|
||||
};
|
||||
|
||||
const handleDownload = async (document: Document, e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
|
||||
// Use fileId if available, otherwise try to use id as fallback
|
||||
const fileId = document.fileId || document.id;
|
||||
|
||||
if (!fileId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Construct filename with extension if available
|
||||
const fileName = document.ext ? `${document.name}.${document.ext}` : document.name;
|
||||
|
||||
await downloadFile(fileId, fileName);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
key={message.id || index}
|
||||
className={`${styles.message} ${styles[`message_${message.role}`]}`}
|
||||
>
|
||||
<div className={styles.message_role}>
|
||||
{message.role === 'user' ? t('chat.you') : message.agentName}
|
||||
</div>
|
||||
<div className={styles.message_content}>
|
||||
{message.content}
|
||||
</div>
|
||||
|
||||
{message.documents && message.documents.length > 0 && (
|
||||
<div className={styles.message_documents}>
|
||||
{message.documents.map((document, docIndex) => (
|
||||
<div
|
||||
key={document.id || docIndex}
|
||||
className={styles.document_item}
|
||||
onClick={() => handleDocumentClick(document)}
|
||||
title={`${t('chat.click_to_open')} ${document.name}`}
|
||||
>
|
||||
<span className={styles.document_icon}>
|
||||
{getFileIcon(document.type, document.ext)}
|
||||
</span>
|
||||
<div className={styles.document_info}>
|
||||
<div className={styles.document_name}>
|
||||
{document.ext ? `${document.name}.${document.ext}` : document.name}
|
||||
</div>
|
||||
<div className={styles.document_meta}>
|
||||
{document.size && (
|
||||
<span className={styles.document_size}>
|
||||
{formatFileSize(document.size)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.document_actions}>
|
||||
<button
|
||||
className={styles.document_action_button}
|
||||
onClick={(e) => handlePreview(document, e)}
|
||||
title={t('chat.preview_document')}
|
||||
>
|
||||
<MdOutlineRemoveRedEye />
|
||||
</button>
|
||||
<button
|
||||
className={styles.document_action_button}
|
||||
onClick={(e) => handleDownload(document, e)}
|
||||
title={t('chat.download_document')}
|
||||
>
|
||||
<FaDownload />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{message.timestamp && (
|
||||
<div className={styles.message_timestamp}>
|
||||
{new Date(message.timestamp).toLocaleTimeString()}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* File Preview Popup */}
|
||||
{previewDocument && (
|
||||
<FilePreviewPopup
|
||||
document={previewDocument}
|
||||
isOpen={isPreviewOpen}
|
||||
onClose={handleClosePreview}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MessageItem;
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
import React from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { MessageListProps } from "./dashboardChatAreaTypes";
|
||||
import MessageItem from "./DashboardChatAreaMessageItem";
|
||||
import WorkflowStatusDisplay from "./DashbaordChatAreaStatusDisplayold";
|
||||
import { useLanguage } from "../../../../contexts/LanguageContext";
|
||||
import styles from './DashboardChatArea.module.css';
|
||||
|
||||
const MessageList: React.FC<MessageListProps> = ({
|
||||
messages,
|
||||
currentWorkflowId,
|
||||
workflowStatus,
|
||||
workflowCompleted,
|
||||
startingWorkflow,
|
||||
startError,
|
||||
messagesError,
|
||||
messagesLoading,
|
||||
onStartNewWorkflow,
|
||||
messagesEndRef,
|
||||
handleRetry,
|
||||
shouldShowRetryButton
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
className={styles.chat_messages}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.2, duration: 0.3, ease: "easeOut" }}
|
||||
>
|
||||
<div className={styles.messages_container}>
|
||||
{startingWorkflow && (
|
||||
<div className={styles.loading_message}>
|
||||
<p>{workflowCompleted && currentWorkflowId ? t('chat.sending_followup', 'Sending follow-up message...') : t('chat.sending_message', 'Sending message...')}</p>
|
||||
</div>
|
||||
)}
|
||||
{startError && (
|
||||
<div className={styles.error_message}>
|
||||
<p>{t('chat.error_prefix', 'Error:')} {startError}</p>
|
||||
</div>
|
||||
)}
|
||||
{messagesError && (
|
||||
<div className={styles.error_message}>
|
||||
<p>{t('chat.error_loading_messages', 'Error loading messages:')} {messagesError}</p>
|
||||
</div>
|
||||
)}
|
||||
{currentWorkflowId && messagesLoading && messages.length === 0 && (
|
||||
<div className={styles.loading_message}>
|
||||
<p>{t('chat.loading_workflow_messages', 'Loading workflow messages...')}</p>
|
||||
</div>
|
||||
)}
|
||||
{messages.length > 0 ? (
|
||||
messages.map((message, index) => (
|
||||
<MessageItem
|
||||
key={message.id || index}
|
||||
message={message}
|
||||
index={index}
|
||||
/>
|
||||
))
|
||||
) : !currentWorkflowId ? (
|
||||
<p className={styles.placeholder_text}>{t('chat.start_conversation', 'Start a conversation by entering a message, selecting a template, or continuing a previous workflow...')}</p>
|
||||
) : null}
|
||||
|
||||
{/* Spacer to push workflow status to bottom when there are fewer messages */}
|
||||
{messages.length < 3 && <div className={styles.messages_spacer} />}
|
||||
|
||||
<WorkflowStatusDisplay
|
||||
currentWorkflowId={currentWorkflowId}
|
||||
workflowStatus={workflowStatus}
|
||||
workflowCompleted={workflowCompleted}
|
||||
onStartNewWorkflow={onStartNewWorkflow}
|
||||
handleRetry={handleRetry}
|
||||
shouldShowRetryButton={shouldShowRetryButton}
|
||||
/>
|
||||
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MessageList;
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
# DashboardChatArea - Modular Structure
|
||||
|
||||
This directory contains the refactored `DashboardChatArea` component, broken down into manageable modules for better maintainability and separation of concerns.
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
DashboardChatArea/
|
||||
├── index.ts # Main export file
|
||||
├── types.ts # TypeScript interfaces and types
|
||||
├── DashboardChatArea.tsx # Main orchestrating component
|
||||
├── useChatLogic.ts # Custom hook with all business logic
|
||||
├── MessageList.tsx # Component for displaying messages
|
||||
├── MessageItem.tsx # Individual message component
|
||||
├── ChatInput.tsx # Input field and send button component
|
||||
├── WorkflowStatusDisplay.tsx # Workflow status and completion UI
|
||||
├── DashboardChatArea.module.css # Shared styles
|
||||
└── README.md # This documentation
|
||||
```
|
||||
|
||||
## Component Responsibilities
|
||||
|
||||
### `DashboardChatArea.tsx` (Main Component)
|
||||
- **Purpose**: Orchestrates all child components
|
||||
- **Responsibilities**:
|
||||
- Uses the `useChatLogic` hook
|
||||
- Renders `MessageList` and `ChatInput` components
|
||||
- Passes props between components
|
||||
- **Size**: ~73 lines (reduced from 278 lines)
|
||||
|
||||
### `useChatLogic.ts` (Custom Hook)
|
||||
- **Purpose**: Contains all business logic and state management
|
||||
- **Responsibilities**:
|
||||
- State management (input value, workflow ID, completion status)
|
||||
- Effects for polling, auto-scroll, prompt handling
|
||||
- Workflow operations (send messages, start workflows)
|
||||
- Event handlers
|
||||
- **Size**: ~196 lines
|
||||
|
||||
### `MessageList.tsx` (Message Display)
|
||||
- **Purpose**: Handles the display of all messages and status indicators
|
||||
- **Responsibilities**:
|
||||
- Renders loading and error states
|
||||
- Maps through messages using `MessageItem`
|
||||
- Includes `WorkflowStatusDisplay`
|
||||
- Handles auto-scroll reference
|
||||
- **Size**: ~73 lines
|
||||
|
||||
### `MessageItem.tsx` (Individual Message)
|
||||
- **Purpose**: Renders a single message
|
||||
- **Responsibilities**:
|
||||
- Message content display
|
||||
- Role-based styling
|
||||
- Timestamp formatting
|
||||
- **Size**: ~32 lines
|
||||
|
||||
### `ChatInput.tsx` (Input Interface)
|
||||
- **Purpose**: Handles user input and send functionality
|
||||
- **Responsibilities**:
|
||||
- Input field with ref handling
|
||||
- Send button with animations
|
||||
- Keyboard event handling
|
||||
- Disabled states
|
||||
- **Size**: ~46 lines
|
||||
|
||||
### `WorkflowStatusDisplay.tsx` (Status UI)
|
||||
- **Purpose**: Shows workflow status and completion states
|
||||
- **Responsibilities**:
|
||||
- Running workflow status
|
||||
- Completion message
|
||||
- "Start New Workflow" button
|
||||
- **Size**: ~38 lines
|
||||
|
||||
### `types.ts` (Type Definitions)
|
||||
- **Purpose**: Centralized TypeScript interfaces
|
||||
- **Responsibilities**:
|
||||
- Component prop interfaces
|
||||
- Data structure types
|
||||
- Shared type definitions
|
||||
- **Size**: ~50 lines
|
||||
|
||||
## Benefits of This Structure
|
||||
|
||||
1. **Separation of Concerns**: Each file has a single, clear responsibility
|
||||
2. **Reusability**: Components can be easily reused or tested independently
|
||||
3. **Maintainability**: Easier to locate and modify specific functionality
|
||||
4. **Readability**: Smaller files are easier to understand and navigate
|
||||
5. **Testing**: Individual components can be unit tested in isolation
|
||||
6. **Type Safety**: Centralized types ensure consistency across components
|
||||
|
||||
## Usage
|
||||
|
||||
Import the main component as before:
|
||||
|
||||
```typescript
|
||||
import DashboardChatArea from './DashboardChatArea';
|
||||
// or
|
||||
import DashboardChatArea, { DashboardChatAreaProps } from './DashboardChatArea';
|
||||
```
|
||||
|
||||
The API remains exactly the same - this refactoring is purely internal and doesn't affect how the component is used by parent components.
|
||||
|
||||
## Development Guidelines
|
||||
|
||||
- **Adding new features**: Consider which component/file is most appropriate
|
||||
- **State changes**: Most state logic should go in `useChatLogic.ts`
|
||||
- **UI changes**: Modify the relevant component file
|
||||
- **New types**: Add to `types.ts`
|
||||
- **Styling**: All styles remain in `DashboardChatArea.module.css`
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
import { Prompt } from "../../../../hooks/usePrompts";
|
||||
import { FileInfo } from "../../../../hooks/useFiles";
|
||||
|
||||
export interface DashboardChatAreaProps {
|
||||
selectedPrompt?: Prompt | null;
|
||||
onPromptUsed?: () => void;
|
||||
onWorkflowIdChange?: (workflowId: string | null) => void;
|
||||
onWorkflowCompletedChange?: (completed: boolean) => void;
|
||||
resumeWorkflowId?: string | null;
|
||||
}
|
||||
|
||||
export interface Document {
|
||||
id?: string;
|
||||
fileId?: number;
|
||||
name: string;
|
||||
url?: string;
|
||||
type?: string;
|
||||
size?: number;
|
||||
downloadUrl?: string;
|
||||
ext?: string;
|
||||
}
|
||||
|
||||
export interface Message {
|
||||
id?: string;
|
||||
role: 'user' | 'assistant' | 'system';
|
||||
agentName: string;
|
||||
content: string;
|
||||
timestamp?: string;
|
||||
documents?: Document[];
|
||||
}
|
||||
|
||||
export interface WorkflowStatus {
|
||||
status: string;
|
||||
currentRound?: number;
|
||||
}
|
||||
|
||||
export interface ChatInputProps {
|
||||
inputValue: string;
|
||||
setInputValue: (value: string) => void;
|
||||
onSend: () => void;
|
||||
onKeyPress: (e: React.KeyboardEvent) => void;
|
||||
isDisabled: boolean;
|
||||
placeholder: string;
|
||||
inputRef: React.RefObject<HTMLTextAreaElement | null>;
|
||||
isWorkflowRunning: boolean;
|
||||
onStopWorkflow: () => void;
|
||||
isStoppingWorkflow: boolean;
|
||||
attachedFiles: FileInfo[];
|
||||
onFileAttach: (file: File) => void;
|
||||
onFileRemove: (fileId: number) => void;
|
||||
onFilesSelect: (files: FileInfo[]) => void;
|
||||
}
|
||||
|
||||
export interface MessageListProps {
|
||||
messages: Message[];
|
||||
currentWorkflowId: string | null;
|
||||
workflowStatus: WorkflowStatus | null;
|
||||
workflowCompleted: boolean;
|
||||
startingWorkflow: boolean;
|
||||
startError: string | null;
|
||||
messagesError: string | null;
|
||||
messagesLoading: boolean;
|
||||
onStartNewWorkflow: () => void;
|
||||
messagesEndRef: React.RefObject<HTMLDivElement | null>;
|
||||
handleRetry: () => Promise<void>;
|
||||
shouldShowRetryButton: () => boolean;
|
||||
}
|
||||
|
||||
export interface WorkflowStatusDisplayProps {
|
||||
currentWorkflowId: string | null;
|
||||
workflowStatus: WorkflowStatus | null;
|
||||
workflowCompleted: boolean;
|
||||
onStartNewWorkflow: () => void;
|
||||
handleRetry: () => Promise<void>;
|
||||
shouldShowRetryButton: () => boolean;
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export { default } from './DashboardChatArea';
|
||||
export type { DashboardChatAreaProps } from './dashboardChatAreaTypes';
|
||||
|
|
@ -0,0 +1,843 @@
|
|||
.chat_area {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
/* Grid Container for Four Quadrants */
|
||||
.grid_container {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
/* Quadrant Base Styles */
|
||||
.quadrant {
|
||||
overflow: hidden;
|
||||
background-color: var(--color-bg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.quadrant_header {
|
||||
padding: 10px 15px;
|
||||
border-bottom: 1px solid var(--color-gray-disabled);
|
||||
background-color: var(--color-surface);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.quadrant_header h3 {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.quadrant_content {
|
||||
flex: 1;
|
||||
padding: 15px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* Specific Quadrant Styles */
|
||||
.messages_quadrant {
|
||||
border-right: 1px solid var(--color-primary);
|
||||
border-bottom: 1px solid var(--color-primary);
|
||||
}
|
||||
|
||||
.file_preview_quadrant {
|
||||
border-bottom: 1px solid var(--color-primary);
|
||||
}
|
||||
|
||||
.input_quadrant {
|
||||
border-right: 1px solid var(--color-primary);
|
||||
}
|
||||
|
||||
.connected_files_quadrant {
|
||||
/* No additional borders needed */
|
||||
}
|
||||
|
||||
/* Resizable Dividers */
|
||||
.vertical_divider {
|
||||
background-color: var(--color-primary);
|
||||
cursor: ew-resize;
|
||||
grid-row: 1 / -1;
|
||||
width: 1px;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.vertical_divider:hover {
|
||||
background-color: var(--color-secondary);
|
||||
width: 3px;
|
||||
}
|
||||
|
||||
.horizontal_divider {
|
||||
background-color: var(--color-primary);
|
||||
cursor: ns-resize;
|
||||
height: 1px;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.horizontal_divider:hover {
|
||||
background-color: var(--color-secondary);
|
||||
height: 3px;
|
||||
}
|
||||
|
||||
/* Messages Quadrant - Remove old styles that conflict */
|
||||
.messages_quadrant .quadrant_content {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Override for MessageList component */
|
||||
.messages_quadrant :global(.chat_messages) {
|
||||
border-radius: 0;
|
||||
margin-bottom: 0;
|
||||
height: 100%;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
/* Input Quadrant - Remove old styles that conflict */
|
||||
.input_quadrant .quadrant_content {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.chat_messages {
|
||||
flex: 1;
|
||||
padding: 15px;
|
||||
border-radius: 15px;
|
||||
margin-bottom: 15px;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
.chat_messages::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.chat_messages::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.chat_messages::-webkit-scrollbar-thumb {
|
||||
background: var(--color-gray-disabled);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.chat_messages::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--color-gray);
|
||||
}
|
||||
|
||||
.messages_container {
|
||||
min-height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
|
||||
}
|
||||
|
||||
.messages_spacer {
|
||||
flex: 1;
|
||||
min-height: 20px;
|
||||
}
|
||||
|
||||
.chat_input {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: flex-end;
|
||||
flex-shrink: 0;
|
||||
flex-direction: column;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.chat_input.drag_over {
|
||||
background-color: var(--color-secondary-disabled);
|
||||
border: 2px dashed var(--color-secondary);
|
||||
border-radius: 12px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.input_row {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: flex-end;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.message_input {
|
||||
flex: 1;
|
||||
padding: 12px 16px;
|
||||
border: 1px solid var(--color-gray-disabled);
|
||||
border-radius: 12px;
|
||||
outline: none;
|
||||
font-size: 14px;
|
||||
font-family: var(--font-family);
|
||||
background-color: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.message_input:focus {
|
||||
border-color: var(--color-secondary);
|
||||
}
|
||||
|
||||
.message_input:disabled {
|
||||
background-color: var(--color-surface);
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.attachment_button {
|
||||
height: 48px;
|
||||
width: 48px;
|
||||
background-color: var(--color-secondary-disabled);
|
||||
color: var(--color-secondary);
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background-color 0.2s ease, border-color 0.2s ease;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.attachment_button:hover {
|
||||
background-color: var(--color-secondary-hover);
|
||||
color: var(--color-bg);
|
||||
}
|
||||
|
||||
.attachment_button:disabled {
|
||||
background-color: var(--color-surface);
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.attached_files {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.attached_file {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 8px;
|
||||
background-color: var(--color-secondary-disabled);
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--color-secondary);
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.attached_file_icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.attached_file_name {
|
||||
max-width: 150px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.attached_file_remove {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--color-gray);
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
margin-left: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
transition: background-color 0.2s ease, color 0.2s ease;
|
||||
}
|
||||
|
||||
.attached_file_remove:hover {
|
||||
background-color: var(--color-gray-disabled);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.send_button {
|
||||
height: 48px;
|
||||
width: 48px;
|
||||
background-color: var(--color-secondary);
|
||||
color: var(--color-bg);
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.send_button:hover {
|
||||
background-color: var(--color-secondary-hover);
|
||||
}
|
||||
|
||||
.send_button_icon {
|
||||
height: 60%;
|
||||
width: 60%;
|
||||
margin: none;
|
||||
padding: none;
|
||||
}
|
||||
|
||||
.send_button:disabled {
|
||||
background-color: var(--color-gray-disabled);
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
border: 1px solid var(--color-gray-disabled);
|
||||
}
|
||||
|
||||
.stop_button {
|
||||
padding: 12px 12px;
|
||||
height: 48px;
|
||||
width: 48px;
|
||||
background-color: var(--color-red);
|
||||
color: var(--color-bg);
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.stop_button:hover {
|
||||
background-color: var(--color-red-hover);
|
||||
}
|
||||
|
||||
.stop_button:disabled {
|
||||
background-color: var(--color-gray-disabled);
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.loading_message {
|
||||
padding: 10px;
|
||||
background-color: var(--color-secondary-disabled);
|
||||
border-left: 4px solid var(--color-secondary);
|
||||
border-radius: 4px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.loading_message p {
|
||||
margin: 0;
|
||||
color: var(--color-secondary);
|
||||
font-size: 14px;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.error_message {
|
||||
padding: 10px;
|
||||
background-color: var(--color-red-disabled);
|
||||
border-left: 4px solid var(--color-red);
|
||||
border-radius: 4px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.error_message p {
|
||||
margin: 0;
|
||||
color: var(--color-red);
|
||||
font-size: 14px;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.message {
|
||||
margin-bottom: 15px;
|
||||
padding: 12px;
|
||||
border-radius: 12px;
|
||||
max-width: 80%;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.message_user {
|
||||
background-color: var(--color-secondary);
|
||||
color: var(--color-bg);
|
||||
margin-left: auto;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.message_assistant {
|
||||
background-color: var(--color-surface);
|
||||
color: var(--color-text);
|
||||
margin-left: 0;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.message_system {
|
||||
background-color: var(--color-primary-disabled);
|
||||
color: var(--color-primary);
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.message_role {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
opacity: 0.8;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.message_content {
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.message_timestamp {
|
||||
font-size: 11px;
|
||||
margin-top: 4px;
|
||||
opacity: 0.6;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.placeholder_text {
|
||||
text-align: center;
|
||||
color: var(--color-gray);
|
||||
font-style: italic;
|
||||
margin: 20px 0;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.workflow_status {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.workflow_status p {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
color: var(--color-gray);
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.retry_container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
background-color: var(--color-red-disabled);
|
||||
border: 1px solid var(--color-red);
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.failed_message {
|
||||
font-size: 14px;
|
||||
color: var(--color-red);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.retry_button {
|
||||
background-color: var(--color-primary);
|
||||
color: var(--color-bg);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 8px 16px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
font-family: var(--font-family);
|
||||
transition: background-color 0.2s ease, transform 0.2s ease;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.retry_button:hover {
|
||||
background-color: var(--color-primary-hover);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.retry_button:disabled {
|
||||
background-color: var(--color-gray-disabled);
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.completion_message {
|
||||
padding: 10px 12px;
|
||||
background-color: var(--color-secondary-disabled);
|
||||
border-left: 4px solid var(--color-secondary);
|
||||
border-radius: 4px;
|
||||
margin-bottom: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.completion_message p {
|
||||
margin: 0 0 10px 0;
|
||||
color: var(--color-secondary);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.new_workflow_button {
|
||||
background-color: var(--color-secondary);
|
||||
color: var(--color-bg);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 8px 16px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.new_workflow_button:hover {
|
||||
background-color: var(--color-secondary-hover);
|
||||
}
|
||||
|
||||
.message_documents {
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.document_item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 10px;
|
||||
background-color: var(--color-bg);
|
||||
border: 1px solid var(--color-gray-disabled);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.document_item:hover {
|
||||
border-color: var(--color-primary);
|
||||
background-color: var(--color-surface);
|
||||
}
|
||||
|
||||
.message_assistant .document_item {
|
||||
background-color: var(--color-bg);
|
||||
border-color: var(--color-gray-disabled);
|
||||
}
|
||||
|
||||
.message_assistant .document_item:hover {
|
||||
border-color: var(--color-primary);
|
||||
background-color: var(--color-surface);
|
||||
}
|
||||
|
||||
.document_icon {
|
||||
font-size: 16px;
|
||||
color: var(--color-secondary);
|
||||
flex-shrink: 0;
|
||||
|
||||
|
||||
}
|
||||
|
||||
.document_info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.document_name {
|
||||
font-weight: 500;
|
||||
color: var(--color-text);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.document_meta {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
font-size: 11px;
|
||||
color: var(--color-gray);
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.document_size {
|
||||
font-size: 11px;
|
||||
color: var(--color-gray);
|
||||
}
|
||||
|
||||
.document_type {
|
||||
font-size: 11px;
|
||||
color: var(--color-gray);
|
||||
}
|
||||
|
||||
.document_actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
opacity: 1;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.document_item:hover .document_actions {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.document_action_button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--color-gray);
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.document_action_button:hover {
|
||||
background-color: var(--color-surface);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.message_assistant .document_action_button {
|
||||
color: var(--color-gray);
|
||||
}
|
||||
|
||||
.message_assistant .document_action_button:hover {
|
||||
background-color: var(--color-surface);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
/* File Preview Quadrant Styles */
|
||||
.empty_state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
color: var(--color-gray);
|
||||
}
|
||||
|
||||
.empty_state small {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.loading_state {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: var(--color-gray);
|
||||
}
|
||||
|
||||
.file_preview {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.file_info h4 {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.file_metadata {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
font-size: 12px;
|
||||
color: var(--color-gray);
|
||||
}
|
||||
|
||||
.preview_modes {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
border-bottom: 1px solid var(--color-gray-disabled);
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.mode_button {
|
||||
padding: 6px 12px;
|
||||
border: 1px solid var(--color-gray-disabled);
|
||||
background: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.mode_button:hover {
|
||||
background: var(--color-surface);
|
||||
}
|
||||
|
||||
.mode_button.active {
|
||||
background: var(--color-secondary);
|
||||
color: white;
|
||||
border-color: var(--color-secondary);
|
||||
}
|
||||
|
||||
.preview_content {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.image_preview {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.text_preview {
|
||||
background: var(--color-surface);
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.document_preview {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.download_link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 16px;
|
||||
background: var(--color-secondary);
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.download_link:hover {
|
||||
background: var(--color-primary);
|
||||
}
|
||||
|
||||
/* Connected Files Quadrant Styles */
|
||||
.files_count {
|
||||
font-size: 12px;
|
||||
color: var(--color-gray);
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.files_list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.file_item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px;
|
||||
border: 1px solid var(--color-gray-disabled);
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
background: var(--color-bg);
|
||||
}
|
||||
|
||||
.file_item:hover {
|
||||
background: var(--color-surface);
|
||||
border-color: var(--color-secondary);
|
||||
}
|
||||
|
||||
.file_item.selected {
|
||||
background: var(--color-secondary-disabled);
|
||||
border-color: var(--color-secondary);
|
||||
}
|
||||
|
||||
.file_icon {
|
||||
font-size: 20px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.file_details {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.file_name {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.file_meta {
|
||||
font-size: 12px;
|
||||
color: var(--color-gray);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.file_actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.action_button {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background-color 0.2s ease;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.action_button:hover {
|
||||
background: var(--color-gray-disabled);
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
.chat-grid {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
gap: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.quadrant {
|
||||
overflow: hidden;
|
||||
background-color: var(--color-bg);
|
||||
border: 1px solid var(--color-gray-disabled);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.divider {
|
||||
background-color: var(--color-primary);
|
||||
cursor: ns-resize;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.vertical-divider {
|
||||
cursor: ew-resize;
|
||||
grid-row: 1 / -1;
|
||||
grid-column: 2;
|
||||
}
|
||||
|
||||
.horizontal-divider {
|
||||
cursor: ns-resize;
|
||||
grid-row: 2;
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.divider:hover {
|
||||
background-color: var(--color-secondary);
|
||||
}
|
||||
|
||||
/* Quadrant specific styles */
|
||||
.messages-quadrant {
|
||||
grid-row: 1;
|
||||
grid-column: 1;
|
||||
border-right: none;
|
||||
border-bottom: none;
|
||||
background-color: #e8f5e8 !important; /* Light green */
|
||||
}
|
||||
|
||||
.file-preview-quadrant {
|
||||
grid-row: 1;
|
||||
grid-column: 3;
|
||||
border-bottom: none;
|
||||
background-color: #ffe8e8 !important; /* Light red */
|
||||
}
|
||||
|
||||
.input-quadrant {
|
||||
grid-row: 3;
|
||||
grid-column: 1;
|
||||
border-right: none;
|
||||
background-color: #f0e8ff !important; /* Light purple */
|
||||
}
|
||||
|
||||
.connected-files-quadrant {
|
||||
grid-row: 3;
|
||||
grid-column: 3;
|
||||
background-color: #e8f0ff !important; /* Light blue */
|
||||
}
|
||||
|
|
@ -0,0 +1,507 @@
|
|||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { useUserFiles, UserFile } from '../../../../hooks/useFiles';
|
||||
import DateienAll from '../../../Dateien/DateienAll';
|
||||
import DateienShared from '../../../Dateien/DateienShared';
|
||||
import DateienCreated from '../../../Dateien/DateienCreated';
|
||||
import DateienUploads from '../../../Dateien/DateienUploads';
|
||||
|
||||
interface AttachedFile {
|
||||
id: number;
|
||||
name: string;
|
||||
size: number;
|
||||
type: string;
|
||||
fileData?: File;
|
||||
objectUrl?: string;
|
||||
}
|
||||
|
||||
interface FileAttachmentPopupProps {
|
||||
onClose: () => void;
|
||||
onFilesSelected: (files: AttachedFile[]) => void;
|
||||
currentAttachedFiles: AttachedFile[];
|
||||
}
|
||||
|
||||
const FileAttachmentPopup: React.FC<FileAttachmentPopupProps> = ({
|
||||
onClose,
|
||||
onFilesSelected,
|
||||
currentAttachedFiles
|
||||
}) => {
|
||||
const [selectedFiles, setSelectedFiles] = useState<AttachedFile[]>(currentAttachedFiles);
|
||||
const [dragOver, setDragOver] = useState(false);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState<'upload' | 'existing'>('upload');
|
||||
const [fileSubTab, setFileSubTab] = useState<'all' | 'uploads' | 'created' | 'shared'>('all');
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const { files: existingFiles, loading: filesLoading } = useUserFiles();
|
||||
|
||||
const handleDragEnter = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
setDragOver(true);
|
||||
};
|
||||
|
||||
const handleDragLeave = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
setDragOver(false);
|
||||
};
|
||||
|
||||
const handleDragOver = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
const handleDrop = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
setDragOver(false);
|
||||
|
||||
const files = Array.from(e.dataTransfer.files);
|
||||
handleFileUpload(files);
|
||||
};
|
||||
|
||||
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = e.target.files ? Array.from(e.target.files) : [];
|
||||
handleFileUpload(files);
|
||||
};
|
||||
|
||||
const handleFileUpload = async (files: File[]) => {
|
||||
setUploading(true);
|
||||
|
||||
// Create file objects with data for preview
|
||||
const uploadedFiles: AttachedFile[] = files.map((file, index) => {
|
||||
const fileObj: AttachedFile = {
|
||||
id: Date.now() + index,
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
type: file.type,
|
||||
fileData: file
|
||||
};
|
||||
|
||||
// Create object URL for images
|
||||
if (file.type.startsWith('image/')) {
|
||||
fileObj.objectUrl = URL.createObjectURL(file);
|
||||
console.log('FileAttachmentPopup: Created object URL for', file.name, ':', fileObj.objectUrl);
|
||||
}
|
||||
|
||||
console.log('FileAttachmentPopup: Created file object:', fileObj.name, 'Has fileData:', !!fileObj.fileData, 'Has objectUrl:', !!fileObj.objectUrl);
|
||||
return fileObj;
|
||||
});
|
||||
|
||||
// Add to selected files
|
||||
setSelectedFiles(prev => [...prev, ...uploadedFiles]);
|
||||
setUploading(false);
|
||||
};
|
||||
|
||||
const toggleFileSelection = (file: UserFile) => {
|
||||
const attachedFile: AttachedFile = {
|
||||
id: file.id,
|
||||
name: file.file_name,
|
||||
size: file.size || 0,
|
||||
type: 'application/octet-stream'
|
||||
};
|
||||
|
||||
setSelectedFiles(prev => {
|
||||
const isSelected = prev.some(f => f.id === file.id);
|
||||
if (isSelected) {
|
||||
return prev.filter(f => f.id !== file.id);
|
||||
} else {
|
||||
return [...prev, attachedFile];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const isFileSelected = (fileId: number) => {
|
||||
return selectedFiles.some(f => f.id === fileId);
|
||||
};
|
||||
|
||||
const handleConfirm = () => {
|
||||
onFilesSelected(selectedFiles);
|
||||
};
|
||||
|
||||
// Cleanup object URLs when component unmounts
|
||||
React.useEffect(() => {
|
||||
return () => {
|
||||
selectedFiles.forEach(file => {
|
||||
if (file.objectUrl) {
|
||||
URL.revokeObjectURL(file.objectUrl);
|
||||
}
|
||||
});
|
||||
};
|
||||
}, []);
|
||||
|
||||
const formatFileSize = (bytes: number): string => {
|
||||
if (bytes < 1024) return bytes + ' B';
|
||||
if (bytes < 1024 * 1024) return Math.round(bytes / 1024) + ' KB';
|
||||
return Math.round(bytes / (1024 * 1024)) + ' MB';
|
||||
};
|
||||
|
||||
// Simplified selectable files list
|
||||
const SelectableFilesList: React.FC<{
|
||||
files: UserFile[];
|
||||
selectedFiles: AttachedFile[];
|
||||
onFileSelect: (file: UserFile) => void;
|
||||
activeTab: string;
|
||||
}> = ({ files, selectedFiles, onFileSelect, activeTab }) => {
|
||||
// Filter files based on active tab
|
||||
const filteredFiles = files.filter(file => {
|
||||
switch (activeTab) {
|
||||
case 'uploads':
|
||||
return file.source === 'user_uploaded';
|
||||
case 'created':
|
||||
return file.source === 'agent_created';
|
||||
case 'shared':
|
||||
return file.source === 'shared_with_me';
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
const isFileSelected = (fileId: number) => {
|
||||
return selectedFiles.some(f => f.id === fileId);
|
||||
};
|
||||
|
||||
const getFileIcon = (fileName: string) => {
|
||||
const extension = fileName.split('.').pop()?.toLowerCase();
|
||||
switch (extension) {
|
||||
case 'pdf': return '📄';
|
||||
case 'doc': case 'docx': return '📝';
|
||||
case 'xls': case 'xlsx': return '📊';
|
||||
case 'jpg': case 'jpeg': case 'png': case 'gif': return '🖼️';
|
||||
case 'mp4': case 'avi': return '🎥';
|
||||
case 'txt': return '📄';
|
||||
default: return '📎';
|
||||
}
|
||||
};
|
||||
|
||||
if (filteredFiles.length === 0) {
|
||||
return (
|
||||
<div style={{ textAlign: 'center', padding: '40px', color: '#666' }}>
|
||||
No files found in this category
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Header */}
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '40px 1fr 80px 100px 80px',
|
||||
padding: '12px',
|
||||
fontWeight: '500',
|
||||
borderBottom: '1px solid #eee',
|
||||
backgroundColor: '#f8f9fa',
|
||||
fontSize: '12px'
|
||||
}}>
|
||||
<div></div>
|
||||
<div>Name</div>
|
||||
<div>Type</div>
|
||||
<div>Size</div>
|
||||
<div>Date</div>
|
||||
</div>
|
||||
|
||||
{/* File List */}
|
||||
<div>
|
||||
{filteredFiles.map(file => (
|
||||
<div
|
||||
key={file.id}
|
||||
onClick={() => onFileSelect(file)}
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '40px 1fr 80px 100px 80px',
|
||||
padding: '12px',
|
||||
borderBottom: '1px solid #f0f0f0',
|
||||
cursor: 'pointer',
|
||||
backgroundColor: isFileSelected(file.id) ? '#e3f2fd' : 'white',
|
||||
border: isFileSelected(file.id) ? '1px solid var(--color-secondary)' : '1px solid transparent',
|
||||
transition: 'all 0.2s ease'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (!isFileSelected(file.id)) {
|
||||
e.currentTarget.style.backgroundColor = '#f8f9fa';
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (!isFileSelected(file.id)) {
|
||||
e.currentTarget.style.backgroundColor = 'white';
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isFileSelected(file.id)}
|
||||
onChange={() => {}}
|
||||
style={{ margin: 0 }}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<span style={{ fontSize: '16px' }}>{getFileIcon(file.file_name)}</span>
|
||||
<span style={{ fontSize: '14px', fontWeight: '500' }}>{file.file_name}</span>
|
||||
</div>
|
||||
<div style={{ fontSize: '12px', color: '#666' }}>{file.action}</div>
|
||||
<div style={{ fontSize: '12px', color: '#666' }}>
|
||||
{file.size ? formatFileSize(file.size) : 'Unknown'}
|
||||
</div>
|
||||
<div style={{ fontSize: '12px', color: '#666' }}>
|
||||
{new Date(file.created_at).toLocaleDateString()}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
zIndex: 1000
|
||||
}}>
|
||||
<div style={{
|
||||
backgroundColor: 'white',
|
||||
borderRadius: '12px',
|
||||
width: '600px',
|
||||
maxHeight: '80vh',
|
||||
overflow: 'hidden',
|
||||
boxShadow: '0 4px 20px rgba(0, 0, 0, 0.15)'
|
||||
}}>
|
||||
{/* Header */}
|
||||
<div style={{
|
||||
padding: '20px',
|
||||
borderBottom: '1px solid #eee',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center'
|
||||
}}>
|
||||
<h3 style={{ margin: 0 }}>Attach Files</h3>
|
||||
<button
|
||||
onClick={onClose}
|
||||
style={{
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
fontSize: '18px',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
borderBottom: '1px solid #eee'
|
||||
}}>
|
||||
<button
|
||||
onClick={() => setActiveTab('upload')}
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: '12px',
|
||||
border: 'none',
|
||||
backgroundColor: activeTab === 'upload' ? '#f0f0f0' : 'white',
|
||||
cursor: 'pointer',
|
||||
borderBottom: activeTab === 'upload' ? '2px solid var(--color-secondary)' : 'none'
|
||||
}}
|
||||
>
|
||||
Upload New Files
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('existing')}
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: '12px',
|
||||
border: 'none',
|
||||
backgroundColor: activeTab === 'existing' ? '#f0f0f0' : 'white',
|
||||
cursor: 'pointer',
|
||||
borderBottom: activeTab === 'existing' ? '2px solid var(--color-secondary)' : 'none'
|
||||
}}
|
||||
>
|
||||
Select Existing Files
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div style={{ padding: '20px', maxHeight: '400px', overflowY: 'auto' }}>
|
||||
{activeTab === 'upload' && (
|
||||
<div>
|
||||
{/* Drag and Drop Area */}
|
||||
<div
|
||||
onDragEnter={handleDragEnter}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDragOver={handleDragOver}
|
||||
onDrop={handleDrop}
|
||||
style={{
|
||||
border: `2px dashed ${dragOver ? 'var(--color-secondary)' : '#ccc'}`,
|
||||
borderRadius: '8px',
|
||||
padding: '40px',
|
||||
textAlign: 'center',
|
||||
backgroundColor: dragOver ? '#f0f8ff' : '#fafafa',
|
||||
marginBottom: '20px',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
>
|
||||
<div style={{ fontSize: '48px', marginBottom: '16px' }}>📁</div>
|
||||
<div style={{ fontSize: '16px', marginBottom: '8px' }}>
|
||||
Drag and drop files here or click to browse
|
||||
</div>
|
||||
<div style={{ fontSize: '12px', color: '#666' }}>
|
||||
Supports all file types
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
multiple
|
||||
onChange={handleFileSelect}
|
||||
style={{ display: 'none' }}
|
||||
/>
|
||||
|
||||
{uploading && (
|
||||
<div style={{ textAlign: 'center', padding: '20px' }}>
|
||||
Uploading files...
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'existing' && (
|
||||
<div>
|
||||
{/* File Category Sub-tabs */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
marginBottom: '16px',
|
||||
borderBottom: '1px solid #eee'
|
||||
}}>
|
||||
{[
|
||||
{ key: 'all', label: 'All Files' },
|
||||
{ key: 'uploads', label: 'Uploads' },
|
||||
{ key: 'created', label: 'AI Created' },
|
||||
{ key: 'shared', label: 'Shared' }
|
||||
].map(tab => (
|
||||
<button
|
||||
key={tab.key}
|
||||
onClick={() => setFileSubTab(tab.key as any)}
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: '8px 12px',
|
||||
border: 'none',
|
||||
backgroundColor: fileSubTab === tab.key ? '#f0f0f0' : 'transparent',
|
||||
cursor: 'pointer',
|
||||
fontSize: '12px',
|
||||
borderBottom: fileSubTab === tab.key ? '2px solid var(--color-secondary)' : '2px solid transparent'
|
||||
}}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{filesLoading ? (
|
||||
<div style={{ textAlign: 'center', padding: '20px' }}>
|
||||
Loading files...
|
||||
</div>
|
||||
) : (
|
||||
<div style={{
|
||||
maxHeight: '300px',
|
||||
overflow: 'auto',
|
||||
border: '1px solid #eee',
|
||||
borderRadius: '6px',
|
||||
position: 'relative'
|
||||
}}>
|
||||
<SelectableFilesList
|
||||
files={existingFiles}
|
||||
selectedFiles={selectedFiles}
|
||||
onFileSelect={toggleFileSelection}
|
||||
activeTab={fileSubTab}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Selection Instructions */}
|
||||
<div style={{
|
||||
marginTop: '12px',
|
||||
padding: '8px',
|
||||
backgroundColor: '#f8f9fa',
|
||||
borderRadius: '4px',
|
||||
fontSize: '12px',
|
||||
color: '#666'
|
||||
}}>
|
||||
💡 Click on files to select them for attachment
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Selected Files Summary */}
|
||||
{selectedFiles.length > 0 && (
|
||||
<div style={{
|
||||
padding: '16px 20px',
|
||||
backgroundColor: '#f8f9fa',
|
||||
borderTop: '1px solid #eee'
|
||||
}}>
|
||||
<div style={{ fontSize: '14px', fontWeight: '500', marginBottom: '8px' }}>
|
||||
Selected Files ({selectedFiles.length}):
|
||||
</div>
|
||||
<div style={{ maxHeight: '100px', overflowY: 'auto' }}>
|
||||
{selectedFiles.map(file => (
|
||||
<div key={file.id} style={{
|
||||
fontSize: '12px',
|
||||
color: '#666',
|
||||
marginBottom: '2px'
|
||||
}}>
|
||||
📎 {file.name} ({formatFileSize(file.size)})
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Footer */}
|
||||
<div style={{
|
||||
padding: '20px',
|
||||
borderTop: '1px solid #eee',
|
||||
display: 'flex',
|
||||
gap: '12px',
|
||||
justifyContent: 'flex-end'
|
||||
}}>
|
||||
<button
|
||||
onClick={onClose}
|
||||
style={{
|
||||
padding: '8px 16px',
|
||||
border: '1px solid #ddd',
|
||||
backgroundColor: 'white',
|
||||
borderRadius: '6px',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleConfirm}
|
||||
disabled={selectedFiles.length === 0}
|
||||
style={{
|
||||
padding: '8px 16px',
|
||||
backgroundColor: selectedFiles.length === 0 ? '#ccc' : 'var(--color-secondary)',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '6px',
|
||||
cursor: selectedFiles.length === 0 ? 'not-allowed' : 'pointer'
|
||||
}}
|
||||
>
|
||||
Attach {selectedFiles.length} File{selectedFiles.length !== 1 ? 's' : ''}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FileAttachmentPopup;
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
import { Prompt } from "../../../../hooks/usePrompts";
|
||||
import { FileInfo } from "../../../../hooks/useFiles";
|
||||
|
||||
export interface DashboardChatAreaProps {
|
||||
selectedPrompt?: Prompt | null;
|
||||
|
|
@ -9,6 +8,26 @@ export interface DashboardChatAreaProps {
|
|||
resumeWorkflowId?: string | null;
|
||||
}
|
||||
|
||||
export interface WorkflowMessage {
|
||||
id: string;
|
||||
role: 'user' | 'assistant' | 'system';
|
||||
content: string;
|
||||
timestamp?: string;
|
||||
fileIds?: number[];
|
||||
}
|
||||
|
||||
export interface FileInfo {
|
||||
id: number;
|
||||
name: string;
|
||||
mimeType: string;
|
||||
size?: number;
|
||||
creationDate?: string;
|
||||
downloadUrl?: string;
|
||||
fileData?: File;
|
||||
objectUrl?: string;
|
||||
}
|
||||
|
||||
// Add old interfaces for compatibility
|
||||
export interface Document {
|
||||
id?: string;
|
||||
fileId?: number;
|
||||
|
|
@ -28,49 +47,3 @@ export interface Message {
|
|||
timestamp?: string;
|
||||
documents?: Document[];
|
||||
}
|
||||
|
||||
export interface WorkflowStatus {
|
||||
status: string;
|
||||
currentRound?: number;
|
||||
}
|
||||
|
||||
export interface ChatInputProps {
|
||||
inputValue: string;
|
||||
setInputValue: (value: string) => void;
|
||||
onSend: () => void;
|
||||
onKeyPress: (e: React.KeyboardEvent) => void;
|
||||
isDisabled: boolean;
|
||||
placeholder: string;
|
||||
inputRef: React.RefObject<HTMLTextAreaElement | null>;
|
||||
isWorkflowRunning: boolean;
|
||||
onStopWorkflow: () => void;
|
||||
isStoppingWorkflow: boolean;
|
||||
attachedFiles: FileInfo[];
|
||||
onFileAttach: (file: File) => void;
|
||||
onFileRemove: (fileId: number) => void;
|
||||
onFilesSelect: (files: FileInfo[]) => void;
|
||||
}
|
||||
|
||||
export interface MessageListProps {
|
||||
messages: Message[];
|
||||
currentWorkflowId: string | null;
|
||||
workflowStatus: WorkflowStatus | null;
|
||||
workflowCompleted: boolean;
|
||||
startingWorkflow: boolean;
|
||||
startError: string | null;
|
||||
messagesError: string | null;
|
||||
messagesLoading: boolean;
|
||||
onStartNewWorkflow: () => void;
|
||||
messagesEndRef: React.RefObject<HTMLDivElement | null>;
|
||||
handleRetry: () => Promise<void>;
|
||||
shouldShowRetryButton: () => boolean;
|
||||
}
|
||||
|
||||
export interface WorkflowStatusDisplayProps {
|
||||
currentWorkflowId: string | null;
|
||||
workflowStatus: WorkflowStatus | null;
|
||||
workflowCompleted: boolean;
|
||||
onStartNewWorkflow: () => void;
|
||||
handleRetry: () => Promise<void>;
|
||||
shouldShowRetryButton: () => boolean;
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@ 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 FilePreviewPopup from "../Dashboard/DashboardChat/DashboardChatArea/DashboardChatAreaFilePreview";
|
||||
import { Document } from "../Dashboard/DashboardChat/DashboardChatArea/dashboardChatAreaTypes";
|
||||
import { useLanguage } from "../../contexts/LanguageContext";
|
||||
|
||||
|
|
|
|||
|
|
@ -1,32 +1,28 @@
|
|||
/* Allgemeine Stile */
|
||||
.sidebarContainer {
|
||||
border-radius: 30px;
|
||||
border: none;
|
||||
border-radius: 0px;
|
||||
border-right: 1px solid var(--color-primary);
|
||||
background: var(--color-bg);
|
||||
box-shadow: 0px 2px 6px 0px rgba(194, 194, 194, 0.10);
|
||||
width: 240px;
|
||||
margin-top: 51px;
|
||||
margin-left: 49px;
|
||||
padding-bottom: 1px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
justify-content: top;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
display: flex;
|
||||
width: 240px;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
flex-shrink: 0;
|
||||
margin: 0 0 30px 0;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.logoContainer {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 80px; /* Fixed height instead of auto */
|
||||
padding: 30px 20px 7px 20px;
|
||||
justify-content: space-between;
|
||||
|
|
@ -38,7 +34,7 @@
|
|||
|
||||
.logoWrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
justify-content: left;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
|
@ -49,24 +45,42 @@
|
|||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
/* Toggle Button Styles */
|
||||
.toggleButton {
|
||||
background: var(--color-primary);
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
color: white;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
.logoText {
|
||||
font-family: var(--font-family);
|
||||
font-size: 35px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
letter-spacing: -0.5px;
|
||||
font-weight: 200;
|
||||
}
|
||||
|
||||
.logoPower {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.logoOn {
|
||||
color: var(--color-secondary);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* Toggle Button Styles */
|
||||
.toggleButton {
|
||||
background: none;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
color: var(--color-text);
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: right;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.toggleButton:hover {
|
||||
background: var(--color-primary-hover);
|
||||
background:none;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
|
|
@ -74,7 +88,7 @@
|
|||
.sidebarContainer.minimized {
|
||||
width: 80px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
justify-content: top;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
|
|
@ -93,7 +107,9 @@
|
|||
}
|
||||
|
||||
.sidebarContainer.minimized .toggleButton {
|
||||
margin: 0 auto; /* Center the toggle button */
|
||||
margin: 0 auto;
|
||||
justify-content: center;
|
||||
/* Center the toggle button */
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -46,7 +46,10 @@ const Sidebar: React.FC<SidebarProps> = ({ data }) => {
|
|||
exit={{ opacity: 0, scale: 0.8 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<img src="/logos/PowerOn_transparent.png" alt="Logo" className={styles.logo} />
|
||||
<div className={styles.logoText}>
|
||||
<span className={styles.logoPower}>Power</span>
|
||||
<span className={styles.logoOn}>On</span>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
.menu li {
|
||||
display: flex;
|
||||
width: 200px;
|
||||
width: 220px;
|
||||
height: 44px;
|
||||
padding: 0 3px 0 15px;
|
||||
align-items: center;
|
||||
|
|
@ -55,7 +55,6 @@
|
|||
padding: 2.292px 2.3px 2.508px 2.292px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-left: 20px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,12 @@
|
|||
.user_section {
|
||||
display: flex;
|
||||
width: 240px;
|
||||
height: 100px; /* Fixed height instead of auto */
|
||||
padding: 20px;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
align-items: left;
|
||||
gap: 8px;
|
||||
font-family: var(--font-family);
|
||||
box-sizing: border-box; /* Include padding in height calculation */
|
||||
margin-bottom: 30px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.user_info {
|
||||
|
|
@ -37,9 +35,10 @@
|
|||
.user_section h1 {
|
||||
margin: 0;
|
||||
font-size: 16pt;
|
||||
line-height: 1.2;
|
||||
line-height: 1.;
|
||||
color: var(--color-text);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 400;
|
||||
transition: opacity 0.3s ease;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
|
@ -85,7 +84,6 @@
|
|||
/* Minimized User Section Styles */
|
||||
.user_section.minimized {
|
||||
width: 46px; /* Match menu item width */
|
||||
height: 100px; /* Same fixed height as expanded */
|
||||
padding: 20px 15px 20px 15px; /* Match menu item padding structure */
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
|
|
|
|||
|
|
@ -45,18 +45,11 @@ const SidebarUser: React.FC<SidebarUserProps> = ({ user, isLoading, error, isMin
|
|||
return (
|
||||
<div className={`${styles.user_section} ${isMinimized ? styles.minimized : ''}`}>
|
||||
<div className={styles.user_info}>
|
||||
<FaUserCircle className={styles.user_icon} />
|
||||
<div className={styles.text_content}>
|
||||
<h1>{ user.name }</h1>
|
||||
<p>Rolle: {user.role}</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
className={styles.logout_button}
|
||||
onClick={handleLogout}
|
||||
>
|
||||
<span className={styles.logout_text}>Logout</span>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,63 +1,24 @@
|
|||
.dashboardContainer {
|
||||
margin: 51px 49px 0 36px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
font-family: var(--font-family);
|
||||
width: 98%;
|
||||
max-height: calc(100vh - 100px);
|
||||
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.chatLogContainer {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
}
|
||||
|
||||
.chatLogContainer.expanded {
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* Height classes for different states */
|
||||
.chatArea15vh {
|
||||
height: 35vh;
|
||||
.chatArea {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.chatArea40vh {
|
||||
height: 60vh;
|
||||
}
|
||||
|
||||
.chatArea45vh {
|
||||
height: 60vh;
|
||||
}
|
||||
|
||||
.chatArea60vh {
|
||||
height: 85vh;
|
||||
}
|
||||
|
||||
.logArea15vh {
|
||||
height: 10vh;
|
||||
}
|
||||
|
||||
.logArea25vh {
|
||||
height: 25vh;
|
||||
}
|
||||
|
||||
.logArea40vh {
|
||||
height: 60vh;
|
||||
}
|
||||
|
||||
.logArea60vh {
|
||||
height: 85vh;
|
||||
}
|
||||
|
||||
.promptArea30vh {
|
||||
height: 30vh;
|
||||
}
|
||||
|
||||
.promptArea40vh {
|
||||
height: 40vh;
|
||||
}
|
||||
|
|
@ -42,58 +42,12 @@ function Dashboard () {
|
|||
}, []);
|
||||
|
||||
// Determine CSS classes based on states
|
||||
const getPromptClass = () => {
|
||||
if (isPromptAreaCollapsed) return '';
|
||||
return isChatExpanded ? styles.promptArea40vh : styles.promptArea30vh;
|
||||
};
|
||||
|
||||
const getChatClass = () => {
|
||||
if (isPromptAreaCollapsed && isChatExpanded) return styles.chatArea45vh;
|
||||
if (!isPromptAreaCollapsed && isChatExpanded) return styles.chatArea15vh;
|
||||
if (isPromptAreaCollapsed && !isChatExpanded) return styles.chatArea60vh;
|
||||
return styles.chatArea40vh;
|
||||
};
|
||||
|
||||
const getLogClass = () => {
|
||||
if (isPromptAreaCollapsed && isChatExpanded) return styles.logArea25vh;
|
||||
if (!isPromptAreaCollapsed && isChatExpanded) return styles.logArea15vh;
|
||||
if (isPromptAreaCollapsed && !isChatExpanded) return styles.logArea60vh;
|
||||
return styles.logArea40vh;
|
||||
};
|
||||
|
||||
// Memoize style objects to prevent infinite re-renders
|
||||
const promptStyle = useMemo(() => ({
|
||||
marginBottom: !isPromptAreaCollapsed ? "0px" : "0"
|
||||
}), [isPromptAreaCollapsed]);
|
||||
|
||||
const chatStyle = useMemo(() => ({
|
||||
width: isChatExpanded ? "100%" : "calc(50% - 10px)",
|
||||
flex: isChatExpanded ? "none" : "1",
|
||||
marginBottom: isChatExpanded ? "0px" : "0"
|
||||
}), [isChatExpanded]);
|
||||
|
||||
const logStyle = useMemo(() => ({
|
||||
width: isChatExpanded ? "100%" : "calc(50% - 10px)",
|
||||
flex: isChatExpanded ? "none" : "1"
|
||||
}), [isChatExpanded]);
|
||||
|
||||
return (
|
||||
<div className={styles.dashboardContainer}>
|
||||
<div
|
||||
className={getPromptClass()}
|
||||
style={promptStyle}
|
||||
>
|
||||
<DashboardPrompt
|
||||
onPromptRun={handlePromptRun}
|
||||
isCollapsed={isPromptAreaCollapsed}
|
||||
onToggleCollapse={() => setIsPromptAreaCollapsed(!isPromptAreaCollapsed)}
|
||||
/>
|
||||
</div>
|
||||
<div className={`${styles.chatLogContainer} ${isChatExpanded ? styles.expanded : ''}`}>
|
||||
<div
|
||||
className={getChatClass()}
|
||||
style={chatStyle}
|
||||
>
|
||||
<div className={styles.chatArea}>
|
||||
<DashboardChat
|
||||
isExpanded={isChatExpanded}
|
||||
onToggleExpand={handleChatToggleExpand}
|
||||
|
|
@ -104,16 +58,6 @@ function Dashboard () {
|
|||
onWorkflowResume={handleWorkflowResume}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={getLogClass()}
|
||||
style={logStyle}
|
||||
>
|
||||
<DashboardLog
|
||||
isExpanded={isChatExpanded}
|
||||
workflowId={currentWorkflowId}
|
||||
workflowCompleted={workflowCompleted}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -59,14 +59,16 @@
|
|||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
background: var(--color-surface);
|
||||
background: var(--color-bg);
|
||||
border-radius: 20px;
|
||||
border: 2px solid var(--color-surface);
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.settingInfo {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: var(--color-text);
|
||||
gap: 5px;
|
||||
flex: 1;
|
||||
}
|
||||
|
|
@ -80,7 +82,7 @@
|
|||
|
||||
.settingDescription {
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-gray);
|
||||
color: var(--color-primary);
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
|
|
@ -90,7 +92,7 @@
|
|||
gap: 12px;
|
||||
padding: 12px 20px;
|
||||
border-radius: 25px;
|
||||
border: 2px solid var(--color-gray-disabled);
|
||||
border: 2px solid var(--color-primary);
|
||||
background: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
cursor: pointer;
|
||||
|
|
@ -107,19 +109,6 @@
|
|||
box-shadow: 0 4px 12px rgba(63, 81, 181, 0.15);
|
||||
}
|
||||
|
||||
.themeToggle.light {
|
||||
background: linear-gradient(135deg, var(--color-bg) 0%, var(--color-surface) 100%);
|
||||
}
|
||||
|
||||
.themeToggle.dark {
|
||||
background: linear-gradient(135deg, var(--color-surface) 0%, var(--color-bg) 100%);
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.themeToggle.dark:hover {
|
||||
border-color: var(--color-primary-hover);
|
||||
box-shadow: 0 4px 12px rgba(178, 102, 255, 0.15);
|
||||
}
|
||||
|
||||
.toggleSlider {
|
||||
display: flex;
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@
|
|||
font-family: var(--font-family);
|
||||
z-index: 0;
|
||||
overflow: hidden;
|
||||
padding: 0 49px 0 0;
|
||||
}
|
||||
|
||||
.homeContainer::before {
|
||||
|
|
@ -22,16 +21,21 @@
|
|||
pointer-events: none;
|
||||
}
|
||||
|
||||
.homeSidebar {
|
||||
height: auto;
|
||||
}
|
||||
.homeContent {
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.body {
|
||||
display: flex;
|
||||
max-width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.homeSidebar {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.homeContent {
|
||||
height: 100vh;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
|
@ -25,6 +25,7 @@ function Home () {
|
|||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.3, ease: "easeInOut" }}
|
||||
style={{ height: "100%", display: "flex", flexDirection: "column" }}
|
||||
>
|
||||
<Outlet />
|
||||
</motion.div>
|
||||
|
|
|
|||
Loading…
Reference in a new issue