new dashboard
This commit is contained in:
parent
8784373e76
commit
84764f932b
38 changed files with 3462 additions and 1087 deletions
7
.gitignore
vendored
7
.gitignore
vendored
|
|
@ -26,4 +26,9 @@ dist-ssr
|
||||||
# Environment files
|
# Environment files
|
||||||
.env
|
.env
|
||||||
.env.local
|
.env.local
|
||||||
.env.*.local
|
<<<<<<< Updated upstream
|
||||||
|
.env.*.local
|
||||||
|
=======
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
>>>>>>> Stashed changes
|
||||||
|
|
|
||||||
37
package.json
37
package.json
|
|
@ -1,16 +1,16 @@
|
||||||
{
|
{
|
||||||
"name": "frontend",
|
"name": "frontend_nyla_new",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite --port 5176",
|
"dev": "vite --port 5176",
|
||||||
"build": "vite build",
|
"build": "tsc -b && vite build",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview"
|
||||||
"start": "node server.js"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
<<<<<<< Updated upstream
|
||||||
"@azure/msal-browser": "^4.12.0",
|
"@azure/msal-browser": "^4.12.0",
|
||||||
"@azure/msal-react": "^3.0.12",
|
"@azure/msal-react": "^3.0.12",
|
||||||
"@xstate/react": "^5.0.0",
|
"@xstate/react": "^5.0.0",
|
||||||
|
|
@ -24,23 +24,22 @@
|
||||||
"jwt-decode": "^4.0.0",
|
"jwt-decode": "^4.0.0",
|
||||||
"motion": "^12.7.3",
|
"motion": "^12.7.3",
|
||||||
"pg": "^8.8.0",
|
"pg": "^8.8.0",
|
||||||
|
=======
|
||||||
|
>>>>>>> Stashed changes
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0"
|
||||||
"react-dropzone": "^14.3.8",
|
|
||||||
"react-icons": "^5.5.0",
|
|
||||||
"react-markdown": "^10.1.0",
|
|
||||||
"react-router-dom": "^7.5.0",
|
|
||||||
"xstate": "^5.18.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.21.0",
|
"@eslint/js": "^9.30.1",
|
||||||
"@types/react": "^19.1.6",
|
"@types/react": "^19.1.8",
|
||||||
"@types/react-dom": "^19.1.5",
|
"@types/react-dom": "^19.1.6",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.6.0",
|
||||||
"eslint": "^9.21.0",
|
"eslint": "^9.30.1",
|
||||||
"eslint-plugin-react-hooks": "^5.1.0",
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.19",
|
"eslint-plugin-react-refresh": "^0.4.20",
|
||||||
"globals": "^15.15.0",
|
"globals": "^16.3.0",
|
||||||
"vite": "^6.2.0"
|
"typescript": "~5.8.3",
|
||||||
|
"typescript-eslint": "^8.35.1",
|
||||||
|
"vite": "^5.4.10"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
:root {
|
:root {
|
||||||
--color-bg: #FFFFFF;
|
--color-bg: #F8F9FA; /* war vorher surface */
|
||||||
--color-surface: #F8F9FA;
|
--color-surface: #EFEDE5; /* war vorher bg */
|
||||||
--color-text: #24262B;
|
--color-text: #181818;
|
||||||
|
|
||||||
--color-primary: #8F00FF;
|
--color-primary: #C7C5B2;
|
||||||
--color-primary-hover: #A020FF;
|
--color-primary-hover: #D9D7C6;
|
||||||
--color-primary-disabled: #D1A6F9;
|
--color-primary-disabled: #E3E2D8;
|
||||||
|
|
||||||
--color-secondary: #3F51B5;
|
--color-secondary: #F25843;
|
||||||
--color-secondary-hover: #5A6CE0;
|
--color-secondary-hover: #FF6A55;
|
||||||
--color-secondary-disabled: #BEC5EB;
|
--color-secondary-disabled: #F5B0A4;
|
||||||
|
|
||||||
--color-red: #D85B65;
|
--color-red: #D85B65;
|
||||||
--color-red-hover: #E77A81;
|
--color-red-hover: #E77A81;
|
||||||
|
|
@ -19,26 +19,26 @@
|
||||||
--color-secondary-red-hover: #D46872;
|
--color-secondary-red-hover: #D46872;
|
||||||
--color-secondary-red-disabled: #E8B7BA;
|
--color-secondary-red-disabled: #E8B7BA;
|
||||||
|
|
||||||
--color-gray: #6C757D;
|
--color-gray: #181818;
|
||||||
--color-gray-hover: #8A9299;
|
--color-gray-hover: #2A2A2A;
|
||||||
--color-gray-disabled: #D6D8DB;
|
--color-gray-disabled: #9B9B9B;
|
||||||
|
|
||||||
--font-family: "Trebuchet MS", sans-serif;
|
--font-family: "DM Sans", sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Dark theme overrides */
|
/* Dark theme overrides */
|
||||||
.dark-theme {
|
.dark-theme {
|
||||||
--color-bg: #121212;
|
--color-bg: #181818; /* war vorher surface */
|
||||||
--color-surface: #1E1E1E;
|
--color-surface: #1E1D1A; /* war vorher bg */
|
||||||
--color-text: #E5E7EB;
|
--color-text: #E5E7EB;
|
||||||
|
|
||||||
--color-primary: #B266FF;
|
--color-primary: #C7C5B2;
|
||||||
--color-primary-hover: #C68AFF;
|
--color-primary-hover: #E0DECC;
|
||||||
--color-primary-disabled: #5C2B80;
|
--color-primary-disabled: #59584F;
|
||||||
|
|
||||||
--color-secondary: #6F7BE5;
|
--color-secondary: #F25843;
|
||||||
--color-secondary-hover: #8592FF;
|
--color-secondary-hover: #FF715C;
|
||||||
--color-secondary-disabled: #3B4370;
|
--color-secondary-disabled: #6E3E36;
|
||||||
|
|
||||||
--color-red: #FF6F7A;
|
--color-red: #FF6F7A;
|
||||||
--color-red-hover: #FF8B94;
|
--color-red-hover: #FF8B94;
|
||||||
|
|
@ -48,8 +48,8 @@
|
||||||
--color-secondary-red-hover: #E17683;
|
--color-secondary-red-hover: #E17683;
|
||||||
--color-secondary-red-disabled: #70363C;
|
--color-secondary-red-disabled: #70363C;
|
||||||
|
|
||||||
--color-gray: #A0A4AA;
|
--color-gray: #181818;
|
||||||
--color-gray-hover: #C4C8CD;
|
--color-gray-hover: #2E2E2E;
|
||||||
--color-gray-disabled: #505357;
|
--color-gray-disabled: #505050;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1,317 +1,16 @@
|
||||||
.dashboard_chat {
|
.dashboard_chat {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
flex-direction: column;
|
flex-direction: column; /* Fixed: was 'space-between' which is invalid */
|
||||||
align-self: stretch;
|
align-self: stretch;
|
||||||
border-radius: 30px;
|
|
||||||
background: var(--color-bg);
|
background: var(--color-bg);
|
||||||
position: relative;
|
position: relative;
|
||||||
box-shadow: 0px 2px 6px 0px rgba(194, 194, 194, 0.10);
|
box-shadow: 0px 2px 6px 0px rgba(194, 194, 194, 0.10);
|
||||||
height: 100%;
|
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
height: 100%; /* Fill parent height */
|
||||||
|
flex: 1; /* Take all available space from parent */
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
font-family: var(--font-family);
|
font-family: var(--font-family);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard_chat.expanded {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat_header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: flex-start;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat_button_div {
|
|
||||||
display: flex;
|
|
||||||
gap: 20px;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.buttonWrapper {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat_button {
|
|
||||||
text-align: center;
|
|
||||||
font-size: 14px;
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 500;
|
|
||||||
line-height: normal;
|
|
||||||
border: none;
|
|
||||||
background: none;
|
|
||||||
outline: none;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 0;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
font-family: var(--font-family);
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat_button_active {
|
|
||||||
color: var(--color-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat_button_inactive {
|
|
||||||
color: var(--color-gray);
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat_button_collapsed {
|
|
||||||
opacity: 50%;
|
|
||||||
color: var(--color-gray);
|
|
||||||
}
|
|
||||||
|
|
||||||
.iconContainer {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.expandIcon, .collapseIcon {
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
color: var(--color-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.expandIcon:hover, .collapseIcon:hover {
|
|
||||||
color: var(--color-secondary-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
.horizontalLine {
|
|
||||||
width: 100%;
|
|
||||||
background-color: var(--color-text);
|
|
||||||
height: 1px;
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.horizontalLineLight {
|
|
||||||
width: calc(100%);
|
|
||||||
background-color: var(--color-gray-disabled);
|
|
||||||
height: 2px;
|
|
||||||
margin-top: 39px;
|
|
||||||
margin-left: -20px;
|
|
||||||
position: absolute;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat_content {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
flex: 1;
|
|
||||||
margin-top: 20px;
|
|
||||||
min-height: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat_messages {
|
|
||||||
flex: 1;
|
|
||||||
padding: 15px;
|
|
||||||
border-radius: 15px;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
min-height: 200px;
|
|
||||||
overflow-y: auto;
|
|
||||||
overflow-x: hidden;
|
|
||||||
scroll-behavior: smooth;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat_input {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
align-items: center;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message_input {
|
|
||||||
flex: 1;
|
|
||||||
padding: 12px 16px;
|
|
||||||
border-radius: 12px;
|
|
||||||
outline: none;
|
|
||||||
font-size: 14px;
|
|
||||||
font-family: var(--font-family);
|
|
||||||
background-color: var(--color-bg);
|
|
||||||
color: var(--color-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.message_input:focus {
|
|
||||||
border-color: var(--color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.send_button {
|
|
||||||
padding: 12px 12px;
|
|
||||||
background-color: var(--color-secondary);
|
|
||||||
color: var(--color-bg);
|
|
||||||
border: none;
|
|
||||||
border-radius: 12px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-family: var(--font-family);
|
|
||||||
}
|
|
||||||
|
|
||||||
.send_button_icon {
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
margin: none;
|
|
||||||
padding: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.send_button:disabled {
|
|
||||||
background-color: var(--color-gray-disabled);
|
|
||||||
cursor: not-allowed;
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message_input:disabled {
|
|
||||||
background-color: var(--color-surface);
|
|
||||||
cursor: not-allowed;
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading_message {
|
|
||||||
padding: 10px;
|
|
||||||
background-color: var(--color-secondary-disabled);
|
|
||||||
border-left: 4px solid var(--color-secondary);
|
|
||||||
border-radius: 4px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading_message p {
|
|
||||||
margin: 0;
|
|
||||||
color: var(--color-secondary);
|
|
||||||
font-size: 14px;
|
|
||||||
font-family: var(--font-family);
|
|
||||||
}
|
|
||||||
|
|
||||||
.error_message {
|
|
||||||
padding: 10px;
|
|
||||||
background-color: var(--color-red-disabled);
|
|
||||||
border-left: 4px solid var(--color-red);
|
|
||||||
border-radius: 4px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error_message p {
|
|
||||||
margin: 0;
|
|
||||||
color: var(--color-red);
|
|
||||||
font-size: 14px;
|
|
||||||
font-family: var(--font-family);
|
|
||||||
}
|
|
||||||
|
|
||||||
.message {
|
|
||||||
margin-bottom: 15px;
|
|
||||||
padding: 12px;
|
|
||||||
border-radius: 12px;
|
|
||||||
max-width: 80%;
|
|
||||||
font-family: var(--font-family);
|
|
||||||
}
|
|
||||||
|
|
||||||
.message_user {
|
|
||||||
background-color: var(--color-secondary);
|
|
||||||
color: var(--color-bg);
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message_assistant {
|
|
||||||
background-color: var(--color-surface);
|
|
||||||
color: var(--color-text);
|
|
||||||
margin-left: 0;
|
|
||||||
margin-right: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message_system {
|
|
||||||
background-color: var(--color-primary-disabled);
|
|
||||||
color: var(--color-primary);
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: auto;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message_role {
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 600;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
opacity: 0.8;
|
|
||||||
font-family: var(--font-family);
|
|
||||||
}
|
|
||||||
|
|
||||||
.message_content {
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.4;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
word-wrap: break-word;
|
|
||||||
font-family: var(--font-family);
|
|
||||||
}
|
|
||||||
|
|
||||||
.message_timestamp {
|
|
||||||
font-size: 11px;
|
|
||||||
margin-top: 4px;
|
|
||||||
opacity: 0.6;
|
|
||||||
font-family: var(--font-family);
|
|
||||||
}
|
|
||||||
|
|
||||||
.placeholder_text {
|
|
||||||
text-align: center;
|
|
||||||
color: var(--color-gray);
|
|
||||||
font-style: italic;
|
|
||||||
margin: 20px 0;
|
|
||||||
font-family: var(--font-family);
|
|
||||||
}
|
|
||||||
|
|
||||||
.workflow_status {
|
|
||||||
padding: 8px 12px;
|
|
||||||
background-color: var(--color-secondary-disabled);
|
|
||||||
border-left: 4px solid var(--color-secondary);
|
|
||||||
border-radius: 4px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.workflow_status p {
|
|
||||||
margin: 0;
|
|
||||||
color: var(--color-secondary);
|
|
||||||
font-size: 13px;
|
|
||||||
font-style: italic;
|
|
||||||
font-family: var(--font-family);
|
|
||||||
}
|
|
||||||
|
|
||||||
.completion_message {
|
|
||||||
padding: 10px 12px;
|
|
||||||
background-color: var(--color-secondary-disabled);
|
|
||||||
border-left: 4px solid var(--color-secondary);
|
|
||||||
border-radius: 4px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.completion_message p {
|
|
||||||
margin: 0 0 10px 0;
|
|
||||||
color: var(--color-secondary);
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 600;
|
|
||||||
font-family: var(--font-family);
|
|
||||||
}
|
|
||||||
|
|
||||||
.new_workflow_button {
|
|
||||||
background-color: var(--color-secondary);
|
|
||||||
color: var(--color-bg);
|
|
||||||
border: none;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 8px 16px;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 500;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background-color 0.2s ease;
|
|
||||||
font-family: var(--font-family);
|
|
||||||
}
|
|
||||||
|
|
||||||
.new_workflow_button:hover {
|
|
||||||
background-color: var(--color-secondary-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -48,91 +48,15 @@ const DashboardChat: React.FC<DashboardChatProps> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<div className={styles.dashboard_chat}>
|
||||||
className={`${styles.dashboard_chat} ${isExpanded ? styles.expanded : ''}`}
|
<DashboardChatArea
|
||||||
layout
|
selectedPrompt={selectedPrompt}
|
||||||
transition={{ duration: 0.4, ease: "easeOut" }}
|
onPromptUsed={onPromptUsed}
|
||||||
>
|
onWorkflowIdChange={onWorkflowIdChange}
|
||||||
<motion.div
|
onWorkflowCompletedChange={onWorkflowCompletedChange}
|
||||||
className={styles.chat_header}
|
resumeWorkflowId={resumeWorkflowId}
|
||||||
layout
|
/>
|
||||||
transition={{ duration: 0.3, ease: "easeOut" }}
|
</div>
|
||||||
>
|
|
||||||
<div className={styles.chat_button_div}>
|
|
||||||
{[t('dashboard.chat.area'), t('dashboard.chat.history')].map((tab) => (
|
|
||||||
<div key={tab} className={styles.buttonWrapper}>
|
|
||||||
<motion.button
|
|
||||||
key={tab}
|
|
||||||
className={`${styles.chat_button} ${
|
|
||||||
activeTab === tab ? styles.chat_button_active : styles.chat_button_inactive
|
|
||||||
}`}
|
|
||||||
onClick={() => setActiveTab(tab)}
|
|
||||||
whileHover={{ scale: 1.05 }}
|
|
||||||
whileTap={{ scale: 0.98 }}
|
|
||||||
>
|
|
||||||
{tab}
|
|
||||||
</motion.button>
|
|
||||||
<AnimatePresence>
|
|
||||||
{activeTab === tab && (
|
|
||||||
<motion.div
|
|
||||||
className={styles.horizontalLine}
|
|
||||||
initial={{ opacity: 0, width: "0%" }}
|
|
||||||
animate={{ opacity: 1, width: "100%" }}
|
|
||||||
exit={{ opacity: 0, width: "0%" }}
|
|
||||||
transition={{ duration: 0.3, ease: "easeOut" }}
|
|
||||||
></motion.div>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className={styles.iconContainer}>
|
|
||||||
<motion.div
|
|
||||||
className={styles.expandIcon}
|
|
||||||
onClick={onToggleExpand}
|
|
||||||
whileTap={{ scale: 0.9 }}
|
|
||||||
whileHover={{ scale: 1.15 }}
|
|
||||||
animate={{ rotate: isExpanded ? 180 : 0 }}
|
|
||||||
transition={{ duration: 0.3, ease: "easeOut" }}
|
|
||||||
>
|
|
||||||
{isExpanded ? <BsArrowsAngleContract size={20} /> : <BsArrowsAngleExpand size={20} />}
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
<motion.div
|
|
||||||
className={styles.horizontalLineLight}
|
|
||||||
initial={{ opacity: 0, scaleX: 0 }}
|
|
||||||
animate={{ opacity: 1, scaleX: 1 }}
|
|
||||||
transition={{ duration: 0.3, ease: "easeOut" }}
|
|
||||||
style={{ transformOrigin: "left" }}
|
|
||||||
></motion.div>
|
|
||||||
<motion.div
|
|
||||||
className={styles.chat_content}
|
|
||||||
initial={{ height: 0, opacity: 0 }}
|
|
||||||
animate={{ height: "auto", opacity: 1 }}
|
|
||||||
transition={{
|
|
||||||
duration: 0.4,
|
|
||||||
ease: [0.4, 0.0, 0.2, 1],
|
|
||||||
height: { duration: 0.4 },
|
|
||||||
opacity: { duration: 0.3 }
|
|
||||||
}}
|
|
||||||
style={{ overflow: "hidden" }}
|
|
||||||
>
|
|
||||||
{activeTab === t('dashboard.chat.area') ? (
|
|
||||||
<DashboardChatArea
|
|
||||||
selectedPrompt={selectedPrompt}
|
|
||||||
onPromptUsed={onPromptUsed}
|
|
||||||
onWorkflowIdChange={onWorkflowIdChange}
|
|
||||||
onWorkflowCompletedChange={onWorkflowCompletedChange}
|
|
||||||
resumeWorkflowId={resumeWorkflowId}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<DashboardChatHistory
|
|
||||||
onWorkflowResume={handleWorkflowResume}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</motion.div>
|
|
||||||
</motion.div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import React, { useEffect } from "react";
|
import React, { useState } from "react";
|
||||||
import { DashboardChatAreaProps } from "./dashboardChatAreaTypes";
|
|
||||||
import { useChatLogic } from "./dashboardChatAreaLogic";
|
|
||||||
import { useLanguage } from "../../../../contexts/LanguageContext";
|
|
||||||
import MessageList from "./DashboardChatAreaMessageList";
|
import MessageList from "./DashboardChatAreaMessageList";
|
||||||
import ChatInput from "./DashboardChatAreaInput";
|
import FilePreview from "./DashboardChatAreaFilePreview";
|
||||||
import styles from './DashboardChatArea.module.css';
|
import InputArea from "./DashboardChatAreaInput";
|
||||||
|
import ConnectedFiles from "./DashboardChatAreaConnectedFiles";
|
||||||
|
import "./DashboardChatAreaStyles/grid.css";
|
||||||
|
import { DashboardChatAreaProps } from "./dashboardChatAreaTypes";
|
||||||
|
|
||||||
const DashboardChatArea: React.FC<DashboardChatAreaProps> = ({
|
const DashboardChatArea: React.FC<DashboardChatAreaProps> = ({
|
||||||
selectedPrompt,
|
selectedPrompt,
|
||||||
|
|
@ -13,92 +13,134 @@ const DashboardChatArea: React.FC<DashboardChatAreaProps> = ({
|
||||||
onWorkflowCompletedChange,
|
onWorkflowCompletedChange,
|
||||||
resumeWorkflowId
|
resumeWorkflowId
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
// Grid sizing state
|
||||||
// State
|
const [horizontalSplit, setHorizontalSplit] = useState(60); // percentage
|
||||||
inputValue,
|
const [verticalSplit, setVerticalSplit] = useState(60); // percentage
|
||||||
setInputValue,
|
const [isDragging, setIsDragging] = useState<'horizontal' | 'vertical' | null>(null);
|
||||||
currentWorkflowId,
|
|
||||||
workflowCompleted,
|
|
||||||
attachedFiles,
|
|
||||||
|
|
||||||
// Refs
|
|
||||||
inputRef,
|
|
||||||
messagesEndRef,
|
|
||||||
|
|
||||||
// Data from hooks
|
|
||||||
messages,
|
|
||||||
messagesLoading,
|
|
||||||
messagesError,
|
|
||||||
startingWorkflow,
|
|
||||||
startError,
|
|
||||||
workflowStatus,
|
|
||||||
|
|
||||||
// Handlers
|
|
||||||
handleSend,
|
|
||||||
handleKeyPress,
|
|
||||||
startNewWorkflow,
|
|
||||||
handleStopWorkflow,
|
|
||||||
handleFileAttach,
|
|
||||||
handleFileRemove,
|
|
||||||
handleFilesSelect,
|
|
||||||
handleRetry,
|
|
||||||
|
|
||||||
// Workflow state
|
|
||||||
isWorkflowRunning,
|
|
||||||
isStoppingWorkflow,
|
|
||||||
shouldShowRetryButton
|
|
||||||
} = useChatLogic({
|
|
||||||
selectedPrompt,
|
|
||||||
onPromptUsed,
|
|
||||||
onWorkflowIdChange,
|
|
||||||
resumeWorkflowId
|
|
||||||
});
|
|
||||||
|
|
||||||
const { t } = useLanguage();
|
// File selection state
|
||||||
|
const [selectedFile, setSelectedFile] = useState<any>(null);
|
||||||
|
const [attachedFiles, setAttachedFiles] = useState<any[]>([]);
|
||||||
|
|
||||||
|
// Workflow state
|
||||||
|
const [currentWorkflowId, setCurrentWorkflowId] = useState<string | null>(resumeWorkflowId || null);
|
||||||
|
|
||||||
// Notify parent component when workflow completion status changes
|
// Handle workflow ID changes
|
||||||
useEffect(() => {
|
const handleWorkflowIdChange = (workflowId: string | null) => {
|
||||||
if (onWorkflowCompletedChange) {
|
setCurrentWorkflowId(workflowId);
|
||||||
onWorkflowCompletedChange(workflowCompleted);
|
if (onWorkflowIdChange) {
|
||||||
|
onWorkflowIdChange(workflowId);
|
||||||
}
|
}
|
||||||
}, [workflowCompleted, onWorkflowCompletedChange]);
|
};
|
||||||
|
|
||||||
const placeholder = workflowCompleted ? t('chat.continue_conversation') : t('chat.enter_message');
|
// Handle resizing
|
||||||
|
const handleMouseDown = (direction: 'horizontal' | 'vertical') => (e: React.MouseEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsDragging(direction);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseMove = (e: MouseEvent) => {
|
||||||
|
if (!isDragging) return;
|
||||||
|
|
||||||
|
const container = document.querySelector('.chat-grid') as HTMLElement;
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
const rect = container.getBoundingClientRect();
|
||||||
|
|
||||||
|
if (isDragging === 'horizontal') {
|
||||||
|
const newSplit = ((e.clientY - rect.top) / rect.height) * 100;
|
||||||
|
setHorizontalSplit(Math.max(20, Math.min(80, newSplit)));
|
||||||
|
} else if (isDragging === 'vertical') {
|
||||||
|
const newSplit = ((e.clientX - rect.left) / rect.width) * 100;
|
||||||
|
setVerticalSplit(Math.max(20, Math.min(80, newSplit)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseUp = () => {
|
||||||
|
setIsDragging(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Event listeners
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (isDragging) {
|
||||||
|
document.addEventListener('mousemove', handleMouseMove);
|
||||||
|
document.addEventListener('mouseup', handleMouseUp);
|
||||||
|
document.body.style.cursor = isDragging === 'horizontal' ? 'ns-resize' : 'ew-resize';
|
||||||
|
document.body.style.userSelect = 'none';
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mousemove', handleMouseMove);
|
||||||
|
document.removeEventListener('mouseup', handleMouseUp);
|
||||||
|
document.body.style.cursor = '';
|
||||||
|
document.body.style.userSelect = '';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [isDragging]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.chat_area}>
|
<div
|
||||||
<MessageList
|
className="chat-grid"
|
||||||
messages={messages}
|
style={{
|
||||||
currentWorkflowId={currentWorkflowId}
|
gridTemplateRows: `${horizontalSplit}% 1px ${100 - horizontalSplit}%`,
|
||||||
workflowStatus={workflowStatus}
|
gridTemplateColumns: `${verticalSplit}% 1px ${100 - verticalSplit}%`
|
||||||
workflowCompleted={workflowCompleted}
|
}}
|
||||||
startingWorkflow={startingWorkflow}
|
>
|
||||||
startError={startError}
|
{/* Top Left: Message List */}
|
||||||
messagesError={messagesError}
|
<div className="quadrant messages-quadrant">
|
||||||
messagesLoading={messagesLoading}
|
<MessageList
|
||||||
onStartNewWorkflow={startNewWorkflow}
|
selectedPrompt={selectedPrompt}
|
||||||
messagesEndRef={messagesEndRef}
|
onPromptUsed={onPromptUsed}
|
||||||
handleRetry={handleRetry}
|
resumeWorkflowId={currentWorkflowId}
|
||||||
shouldShowRetryButton={shouldShowRetryButton}
|
onFilePreview={setSelectedFile}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Vertical Divider */}
|
||||||
|
<div
|
||||||
|
className="divider vertical-divider"
|
||||||
|
onMouseDown={handleMouseDown('vertical')}
|
||||||
/>
|
/>
|
||||||
<ChatInput
|
|
||||||
inputValue={inputValue}
|
{/* Top Right: File Preview */}
|
||||||
setInputValue={setInputValue}
|
<div className="quadrant file-preview-quadrant">
|
||||||
onSend={handleSend}
|
<FilePreview selectedFile={selectedFile} />
|
||||||
onKeyPress={handleKeyPress}
|
</div>
|
||||||
isDisabled={startingWorkflow}
|
|
||||||
placeholder={placeholder}
|
{/* Horizontal Divider */}
|
||||||
inputRef={inputRef}
|
<div
|
||||||
isWorkflowRunning={isWorkflowRunning}
|
className="divider horizontal-divider"
|
||||||
onStopWorkflow={handleStopWorkflow}
|
onMouseDown={handleMouseDown('horizontal')}
|
||||||
isStoppingWorkflow={isStoppingWorkflow}
|
|
||||||
attachedFiles={attachedFiles}
|
|
||||||
onFileAttach={handleFileAttach}
|
|
||||||
onFileRemove={handleFileRemove}
|
|
||||||
onFilesSelect={handleFilesSelect}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Bottom Left: Input Area */}
|
||||||
|
<div className="quadrant input-quadrant">
|
||||||
|
<InputArea
|
||||||
|
selectedPrompt={selectedPrompt}
|
||||||
|
onPromptUsed={onPromptUsed}
|
||||||
|
onWorkflowIdChange={handleWorkflowIdChange}
|
||||||
|
onAttachedFilesChange={setAttachedFiles}
|
||||||
|
attachedFiles={attachedFiles}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bottom Right: Connected Files */}
|
||||||
|
<div className="quadrant connected-files-quadrant">
|
||||||
|
<ConnectedFiles
|
||||||
|
onFileSelect={setSelectedFile}
|
||||||
|
selectedFile={selectedFile}
|
||||||
|
attachedFiles={attachedFiles}
|
||||||
|
onRemoveFile={(fileId) => {
|
||||||
|
// If the removed file is currently selected, clear the selection
|
||||||
|
if (selectedFile?.id === fileId) {
|
||||||
|
setSelectedFile(null);
|
||||||
|
}
|
||||||
|
// Remove the file from attached files
|
||||||
|
setAttachedFiles(files => files.filter(f => f.id !== fileId));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DashboardChatArea;
|
export default DashboardChatArea;
|
||||||
|
|
|
||||||
|
|
@ -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 React, { useState, useEffect } from 'react';
|
||||||
import { motion } from "framer-motion";
|
import { useWorkflowOperations } from '../../../../hooks/useWorkflows';
|
||||||
import { LuSendHorizontal } from "react-icons/lu";
|
import { Prompt } from '../../../../hooks/usePrompts';
|
||||||
import { FaStop } from "react-icons/fa";
|
import FileAttachmentPopup from './FileAttachmentPopup';
|
||||||
import { IoAttach, IoClose } from "react-icons/io5";
|
|
||||||
import { ChatInputProps } from "./dashboardChatAreaTypes";
|
|
||||||
import { FileInfo } from "../../../../hooks/useFiles";
|
|
||||||
import { useLanguage } from "../../../../contexts/LanguageContext";
|
|
||||||
import DateienSelector from "../../../Dateien/DateienHinzufügen/DateienSelector";
|
|
||||||
import styles from './DashboardChatArea.module.css';
|
|
||||||
|
|
||||||
// Helper function to get file icon based on type
|
interface InputAreaProps {
|
||||||
const getFileIcon = (mimeType?: string): string => {
|
selectedPrompt?: Prompt | null;
|
||||||
if (!mimeType) return '📄';
|
onPromptUsed?: () => void;
|
||||||
|
onWorkflowIdChange?: (workflowId: string | null) => void;
|
||||||
const type = mimeType.toLowerCase();
|
onAttachedFilesChange?: (files: AttachedFile[]) => void;
|
||||||
|
attachedFiles?: AttachedFile[];
|
||||||
if (type.includes('image')) return '🖼️';
|
}
|
||||||
if (type.includes('video')) return '🎥';
|
|
||||||
if (type.includes('audio')) return '🎵';
|
|
||||||
if (type.includes('pdf')) return '📕';
|
|
||||||
if (type.includes('word') || type.includes('document')) return '📘';
|
|
||||||
if (type.includes('excel') || type.includes('spreadsheet')) return '📊';
|
|
||||||
if (type.includes('powerpoint') || type.includes('presentation')) return '📋';
|
|
||||||
if (type.includes('text')) return '📝';
|
|
||||||
if (type.includes('zip') || type.includes('archive')) return '📦';
|
|
||||||
if (type.includes('javascript') || type.includes('json') || type.includes('html') || type.includes('css')) return '💻';
|
|
||||||
|
|
||||||
return '📄';
|
|
||||||
};
|
|
||||||
|
|
||||||
const ChatInput: React.FC<ChatInputProps> = ({
|
interface AttachedFile {
|
||||||
inputValue,
|
id: number;
|
||||||
setInputValue,
|
name: string;
|
||||||
onSend,
|
size: number;
|
||||||
onKeyPress,
|
type: string;
|
||||||
isDisabled,
|
fileData?: File;
|
||||||
placeholder,
|
objectUrl?: string;
|
||||||
inputRef,
|
}
|
||||||
isWorkflowRunning,
|
|
||||||
onStopWorkflow,
|
const InputArea: React.FC<InputAreaProps> = ({
|
||||||
isStoppingWorkflow,
|
selectedPrompt,
|
||||||
attachedFiles,
|
onPromptUsed,
|
||||||
onFileAttach,
|
onWorkflowIdChange,
|
||||||
onFileRemove,
|
onAttachedFilesChange,
|
||||||
onFilesSelect
|
attachedFiles: externalAttachedFiles = []
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useLanguage();
|
const [inputValue, setInputValue] = useState('');
|
||||||
const [isUploadModalOpen, setIsUploadModalOpen] = useState(false);
|
const [showFilePopup, setShowFilePopup] = useState(false);
|
||||||
const [isDragOver, setIsDragOver] = useState(false);
|
|
||||||
|
|
||||||
// Auto-resize textarea functionality
|
// Always use external attached files from parent component
|
||||||
|
const currentAttachedFiles = externalAttachedFiles;
|
||||||
|
const { startWorkflow, startingWorkflow, startError } = useWorkflowOperations();
|
||||||
|
|
||||||
|
// Auto-fill input when prompt is selected
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (inputRef?.current) {
|
if (selectedPrompt) {
|
||||||
const textarea = inputRef.current;
|
setInputValue(selectedPrompt.content);
|
||||||
textarea.style.height = 'auto';
|
|
||||||
|
|
||||||
// Calculate line height - approximately 1.5em per line
|
|
||||||
const lineHeight = 24; // Adjust this value based on your CSS line-height
|
|
||||||
const maxHeight = lineHeight * 8; // 8 lines maximum
|
|
||||||
|
|
||||||
const newHeight = Math.min(textarea.scrollHeight, maxHeight);
|
|
||||||
textarea.style.height = `${newHeight}px`;
|
|
||||||
|
|
||||||
// Enable/disable scroll based on content height
|
|
||||||
if (textarea.scrollHeight > maxHeight) {
|
|
||||||
textarea.style.overflowY = 'auto';
|
|
||||||
} else {
|
|
||||||
textarea.style.overflowY = 'hidden';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, [inputValue, inputRef]);
|
}, [selectedPrompt]);
|
||||||
|
|
||||||
const handleAttachmentClick = () => {
|
const handleSend = async () => {
|
||||||
setIsUploadModalOpen(true);
|
if (!inputValue.trim() || startingWorkflow) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await startWorkflow({
|
||||||
|
prompt: inputValue,
|
||||||
|
listFileId: currentAttachedFiles.map(f => f.id)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
setInputValue('');
|
||||||
|
if (onAttachedFilesChange) {
|
||||||
|
onAttachedFilesChange([]);
|
||||||
|
}
|
||||||
|
if (onPromptUsed) onPromptUsed();
|
||||||
|
if (onWorkflowIdChange && result.data?.id) {
|
||||||
|
onWorkflowIdChange(result.data.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to start workflow:', error);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFilesSelected = (files: FileInfo[]) => {
|
const handleKeyPress = (e: React.KeyboardEvent) => {
|
||||||
onFilesSelect(files);
|
|
||||||
setIsUploadModalOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFileRemove = (fileId: number) => {
|
|
||||||
onFileRemove(fileId);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle Enter key press for sending message (without Shift)
|
|
||||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
|
||||||
if (e.key === 'Enter' && !e.shiftKey) {
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!isDisabled && (inputValue.trim() || attachedFiles.length > 0)) {
|
handleSend();
|
||||||
onSend();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Call original onKeyPress if it exists (for backward compatibility)
|
|
||||||
if (onKeyPress && e.key !== 'Enter') {
|
|
||||||
onKeyPress(e as any);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Drag and drop handlers
|
const handleFilesAttached = (files: AttachedFile[]) => {
|
||||||
const handleDragEnter = (e: React.DragEvent) => {
|
setShowFilePopup(false);
|
||||||
e.preventDefault();
|
if (onAttachedFilesChange) {
|
||||||
e.stopPropagation();
|
onAttachedFilesChange(files);
|
||||||
if (!isDisabled && !isWorkflowRunning) {
|
|
||||||
setIsDragOver(true);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDragOver = (e: React.DragEvent) => {
|
const formatFileSize = (bytes: number): string => {
|
||||||
e.preventDefault();
|
if (bytes < 1024) return bytes + ' B';
|
||||||
e.stopPropagation();
|
if (bytes < 1024 * 1024) return Math.round(bytes / 1024) + ' KB';
|
||||||
};
|
return Math.round(bytes / (1024 * 1024)) + ' MB';
|
||||||
|
|
||||||
const handleDragLeave = (e: React.DragEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
// Only set drag over to false if we're leaving the entire input area
|
|
||||||
if (!e.currentTarget.contains(e.relatedTarget as Node)) {
|
|
||||||
setIsDragOver(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDrop = (e: React.DragEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
setIsDragOver(false);
|
|
||||||
|
|
||||||
if (isDisabled || isWorkflowRunning) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const files = Array.from(e.dataTransfer.files);
|
|
||||||
if (files.length > 0) {
|
|
||||||
// Convert File objects to FileInfo objects
|
|
||||||
const fileInfos: FileInfo[] = files.map((file, index) => ({
|
|
||||||
id: Date.now() + index, // Generate unique IDs
|
|
||||||
name: file.name,
|
|
||||||
mimeType: file.type,
|
|
||||||
size: file.size,
|
|
||||||
creationDate: new Date().toISOString(),
|
|
||||||
source: 'user_uploaded'
|
|
||||||
}));
|
|
||||||
|
|
||||||
onFilesSelect(fileInfos);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<div style={{ padding: '16px', height: '100%', display: 'flex', flexDirection: 'column' }}>
|
||||||
className={`${styles.chat_input} ${isDragOver ? styles.drag_over : ''}`}
|
<h3>Input</h3>
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
{startError && (
|
||||||
transition={{ delay: 0.3, duration: 0.3, ease: "easeOut" }}
|
<div style={{
|
||||||
onDragEnter={handleDragEnter}
|
padding: '8px',
|
||||||
onDragOver={handleDragOver}
|
backgroundColor: '#ffe6e6',
|
||||||
onDragLeave={handleDragLeave}
|
color: '#d00',
|
||||||
onDrop={handleDrop}
|
borderRadius: '4px',
|
||||||
>
|
marginBottom: '12px'
|
||||||
{/* Show attached files if any */}
|
}}>
|
||||||
{attachedFiles.length > 0 && (
|
Error: {startError}
|
||||||
<div className={styles.attached_files}>
|
|
||||||
{attachedFiles.map((file) => (
|
|
||||||
<div key={file.id} className={styles.attached_file}>
|
|
||||||
<span className={styles.attached_file_icon}>
|
|
||||||
{getFileIcon(file.mimeType)}
|
|
||||||
</span>
|
|
||||||
<span className={styles.attached_file_name}>
|
|
||||||
{file.name}
|
|
||||||
</span>
|
|
||||||
<button
|
|
||||||
className={styles.attached_file_remove}
|
|
||||||
onClick={() => handleFileRemove(file.id)}
|
|
||||||
title={t('chat.remove_file')}
|
|
||||||
>
|
|
||||||
<IoClose size={12} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Input row with text input, attachment button, and send button */}
|
{/* Show attached files count */}
|
||||||
<div className={styles.input_row}>
|
{currentAttachedFiles.length > 0 && (
|
||||||
<textarea
|
<div style={{
|
||||||
ref={inputRef}
|
marginBottom: '8px',
|
||||||
|
padding: '6px 10px',
|
||||||
|
backgroundColor: '#e3f2fd',
|
||||||
|
borderRadius: '4px',
|
||||||
|
fontSize: '12px',
|
||||||
|
color: '#1976d2',
|
||||||
|
textAlign: 'center'
|
||||||
|
}}>
|
||||||
|
📎 {currentAttachedFiles.length} file{currentAttachedFiles.length !== 1 ? 's' : ''} attached
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px', flex: 1 }}>
|
||||||
|
<textarea
|
||||||
value={inputValue}
|
value={inputValue}
|
||||||
onChange={(e) => setInputValue(e.target.value)}
|
onChange={(e) => setInputValue(e.target.value)}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyPress={handleKeyPress}
|
||||||
placeholder={placeholder}
|
placeholder="Enter your message or prompt..."
|
||||||
className={styles.message_input}
|
disabled={startingWorkflow}
|
||||||
disabled={isDisabled}
|
|
||||||
rows={1}
|
|
||||||
style={{
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
padding: '12px',
|
||||||
|
border: '1px solid var(--color-gray-disabled)',
|
||||||
|
borderRadius: '8px',
|
||||||
resize: 'none',
|
resize: 'none',
|
||||||
minHeight: '24px',
|
fontSize: '14px',
|
||||||
lineHeight: '24px'
|
fontFamily: 'inherit'
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Attachment button */}
|
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
|
||||||
<motion.button
|
<button
|
||||||
className={styles.attachment_button}
|
onClick={() => setShowFilePopup(true)}
|
||||||
onClick={handleAttachmentClick}
|
style={{
|
||||||
disabled={isDisabled || isWorkflowRunning}
|
padding: '8px 12px',
|
||||||
whileTap={{ scale: 0.95 }}
|
backgroundColor: 'var(--color-surface)',
|
||||||
transition={{ duration: 0.2, ease: "easeOut" }}
|
border: '1px solid var(--color-gray-disabled)',
|
||||||
title={t('chat.attach_file')}
|
borderRadius: '6px',
|
||||||
>
|
cursor: 'pointer',
|
||||||
<IoAttach size={26} />
|
fontSize: '14px'
|
||||||
</motion.button>
|
}}
|
||||||
|
>
|
||||||
{/* Send/Stop button */}
|
📎 Attach Files
|
||||||
<motion.button
|
</button>
|
||||||
className={isWorkflowRunning ? styles.stop_button : styles.send_button}
|
|
||||||
onClick={isWorkflowRunning ? onStopWorkflow : onSend}
|
<button
|
||||||
disabled={isWorkflowRunning ? isStoppingWorkflow : (isDisabled || (!inputValue.trim() && attachedFiles.length === 0))}
|
onClick={handleSend}
|
||||||
whileTap={{ scale: 0.95 }}
|
disabled={!inputValue.trim() || startingWorkflow}
|
||||||
transition={{ duration: 0.2, ease: "easeOut" }}
|
style={{
|
||||||
>
|
padding: '8px 16px',
|
||||||
{isWorkflowRunning ? (
|
backgroundColor: startingWorkflow ? 'var(--color-gray-disabled)' : 'var(--color-secondary)',
|
||||||
<FaStop className={styles.send_button_icon}/>
|
color: 'white',
|
||||||
) : (
|
border: 'none',
|
||||||
<LuSendHorizontal className={styles.send_button_icon}/>
|
borderRadius: '6px',
|
||||||
|
cursor: startingWorkflow ? 'not-allowed' : 'pointer'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{startingWorkflow ? 'Starting...' : 'Send'}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{selectedPrompt && (
|
||||||
|
<span style={{ fontSize: '12px', color: 'var(--color-gray)' }}>
|
||||||
|
Using prompt: {selectedPrompt.name}
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
</motion.button>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Upload Modal */}
|
{/* File Attachment Popup */}
|
||||||
<DateienSelector
|
{showFilePopup && (
|
||||||
isOpen={isUploadModalOpen}
|
<FileAttachmentPopup
|
||||||
onClose={() => setIsUploadModalOpen(false)}
|
onClose={() => setShowFilePopup(false)}
|
||||||
onFilesSelected={handleFilesSelected}
|
onFilesSelected={handleFilesAttached}
|
||||||
/>
|
currentAttachedFiles={currentAttachedFiles}
|
||||||
</motion.div>
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ChatInput;
|
export default InputArea;
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,14 @@
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { FaDownload } from "react-icons/fa";
|
import { FaDownload } from "react-icons/fa";
|
||||||
import { MdOutlineRemoveRedEye } from "react-icons/md";
|
import { MdOutlineRemoveRedEye } from "react-icons/md";
|
||||||
import { Message, Document } from "./dashboardChatAreaTypes";
|
|
||||||
import FilePreviewPopup from "./FilePreviewPopup";
|
|
||||||
import { useFileDownload } from "../../../../hooks/useWorkflows";
|
import { useFileDownload } from "../../../../hooks/useWorkflows";
|
||||||
import { useLanguage } from "../../../../contexts/LanguageContext";
|
import { useLanguage } from "../../../../contexts/LanguageContext";
|
||||||
import styles from './DashboardChatArea.module.css';
|
import { Message, Document } from "./dashboardChatAreaTypes";
|
||||||
|
|
||||||
interface MessageItemProps {
|
interface MessageItemProps {
|
||||||
message: Message;
|
message: Message;
|
||||||
index: number;
|
index: number;
|
||||||
|
onFilePreview?: (file: any) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to format file size
|
// Helper function to format file size
|
||||||
|
|
@ -62,15 +61,22 @@ const getFileIcon = (type?: string, ext?: string): string => {
|
||||||
return '📄';
|
return '📄';
|
||||||
};
|
};
|
||||||
|
|
||||||
const MessageItem: React.FC<MessageItemProps> = ({ message, index }) => {
|
const MessageItem: React.FC<MessageItemProps> = ({ message, index, onFilePreview }) => {
|
||||||
const { t } = useLanguage();
|
const { t } = useLanguage();
|
||||||
const [previewDocument, setPreviewDocument] = useState<Document | null>(null);
|
|
||||||
const [isPreviewOpen, setIsPreviewOpen] = useState(false);
|
|
||||||
const { downloadFile, isDownloading, error: downloadError } = useFileDownload();
|
const { downloadFile, isDownloading, error: downloadError } = useFileDownload();
|
||||||
|
|
||||||
|
// Debug: Log what the MessageItem is receiving
|
||||||
|
console.log(`🎭 MessageItem rendering:`, {
|
||||||
|
messageId: message.id,
|
||||||
|
messageRole: message.role,
|
||||||
|
hasDocuments: !!(message.documents),
|
||||||
|
documentsArray: message.documents,
|
||||||
|
documentsLength: message.documents?.length || 0,
|
||||||
|
documentsCheck: message.documents && message.documents.length > 0
|
||||||
|
});
|
||||||
|
|
||||||
const handleDocumentClick = (document: Document) => {
|
const handleDocumentClick = (document: Document) => {
|
||||||
|
console.log(`🖱️ Document clicked:`, document);
|
||||||
// If there's a downloadUrl, use it; otherwise try the url
|
// If there's a downloadUrl, use it; otherwise try the url
|
||||||
const downloadLink = document.downloadUrl || document.url;
|
const downloadLink = document.downloadUrl || document.url;
|
||||||
|
|
||||||
|
|
@ -83,109 +89,184 @@ const MessageItem: React.FC<MessageItemProps> = ({ message, index }) => {
|
||||||
const handlePreview = (document: Document, e: React.MouseEvent) => {
|
const handlePreview = (document: Document, e: React.MouseEvent) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
// Use fileId if available, otherwise try to use id as fallback
|
console.log(`👁️ Preview requested for:`, document);
|
||||||
const fileId = document.fileId || document.id;
|
|
||||||
|
|
||||||
if (!fileId) {
|
// Use fileId if available, otherwise try to use id as fallback
|
||||||
|
const fileId = document.fileId || parseInt(document.id || '0');
|
||||||
|
|
||||||
|
if (!fileId || isNaN(fileId)) {
|
||||||
|
console.error('❌ Invalid file ID for preview:', document);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setPreviewDocument(document);
|
console.log('✅ MessageItem - Previewing file:', { fileId, document });
|
||||||
setIsPreviewOpen(true);
|
|
||||||
};
|
// Call the parent callback to show preview in the file preview quadrant
|
||||||
|
if (onFilePreview) {
|
||||||
const handleClosePreview = () => {
|
onFilePreview({
|
||||||
setIsPreviewOpen(false);
|
id: fileId.toString(),
|
||||||
setPreviewDocument(null);
|
name: document.name,
|
||||||
|
mimeType: document.type || 'application/octet-stream',
|
||||||
|
size: document.size,
|
||||||
|
fileId: fileId
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDownload = async (document: Document, e: React.MouseEvent) => {
|
const handleDownload = async (document: Document, e: React.MouseEvent) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
|
console.log(`⬇️ Download requested for:`, document);
|
||||||
|
|
||||||
// Use fileId if available, otherwise try to use id as fallback
|
// Use fileId if available, otherwise try to use id as fallback
|
||||||
const fileId = document.fileId || document.id;
|
const fileId = document.fileId || parseInt(document.id || '0');
|
||||||
|
|
||||||
if (!fileId) {
|
if (!fileId) {
|
||||||
|
console.error('❌ No file ID for download:', document);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Construct filename with extension if available
|
// Construct filename with extension if available
|
||||||
const fileName = document.ext ? `${document.name}.${document.ext}` : document.name;
|
const fileName = document.ext ? `${document.name}.${document.ext}` : document.name;
|
||||||
|
|
||||||
|
console.log(`💾 Downloading file ${fileId} as "${fileName}"`);
|
||||||
await downloadFile(fileId, fileName);
|
await downloadFile(fileId, fileName);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Debug: Log document check before rendering
|
||||||
|
const hasDocuments = message.documents && message.documents.length > 0;
|
||||||
|
console.log(`🔍 About to check documents:`, {
|
||||||
|
hasDocuments: !!(message.documents),
|
||||||
|
documentsLength: message.documents?.length || 0,
|
||||||
|
willRenderFiles: hasDocuments
|
||||||
|
});
|
||||||
|
|
||||||
|
// Log if no documents
|
||||||
|
if (!hasDocuments) {
|
||||||
|
console.log(`📭 No documents to render for message ${message.id}`);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={message.id || index}
|
style={{
|
||||||
className={`${styles.message} ${styles[`message_${message.role}`]}`}
|
padding: '12px',
|
||||||
|
borderRadius: '8px',
|
||||||
|
backgroundColor: message.role === 'user'
|
||||||
|
? 'var(--color-secondary-disabled)'
|
||||||
|
: 'var(--color-surface)',
|
||||||
|
marginBottom: '8px'
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div className={styles.message_role}>
|
<div style={{
|
||||||
{message.role === 'user' ? t('chat.you') : message.agentName}
|
fontSize: '12px',
|
||||||
|
color: 'var(--color-gray)',
|
||||||
|
marginBottom: '4px',
|
||||||
|
fontWeight: '500'
|
||||||
|
}}>
|
||||||
|
{message.role === 'user' ? 'You' : message.agentName}
|
||||||
|
{message.timestamp && ` • ${new Date(message.timestamp).toLocaleTimeString()}`}
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.message_content}>
|
|
||||||
|
<div style={{
|
||||||
|
lineHeight: '1.5',
|
||||||
|
whiteSpace: 'pre-wrap'
|
||||||
|
}}>
|
||||||
{message.content}
|
{message.content}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{message.documents && message.documents.length > 0 && (
|
{hasDocuments && (
|
||||||
<div className={styles.message_documents}>
|
<div style={{
|
||||||
{message.documents.map((document, docIndex) => (
|
marginTop: '12px',
|
||||||
<div
|
padding: '8px',
|
||||||
key={document.id || docIndex}
|
backgroundColor: 'var(--color-bg)',
|
||||||
className={styles.document_item}
|
borderRadius: '6px',
|
||||||
onClick={() => handleDocumentClick(document)}
|
border: '1px solid var(--color-gray-disabled)'
|
||||||
title={`${t('chat.click_to_open')} ${document.name}`}
|
}}>
|
||||||
>
|
<div style={{
|
||||||
<span className={styles.document_icon}>
|
fontSize: '12px',
|
||||||
{getFileIcon(document.type, document.ext)}
|
color: 'var(--color-gray)',
|
||||||
</span>
|
marginBottom: '8px',
|
||||||
<div className={styles.document_info}>
|
fontWeight: '500'
|
||||||
<div className={styles.document_name}>
|
}}>
|
||||||
{document.ext ? `${document.name}.${document.ext}` : document.name}
|
📎 Attached Files ({message.documents!.length})
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.document_meta}>
|
<div>
|
||||||
|
{message.documents!.map((document, docIndex) => {
|
||||||
|
console.log(`📄 Rendering document ${docIndex + 1}:`, document);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={document.id || docIndex}
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '8px',
|
||||||
|
padding: '6px',
|
||||||
|
borderRadius: '4px',
|
||||||
|
backgroundColor: 'var(--color-surface)',
|
||||||
|
marginBottom: '4px',
|
||||||
|
cursor: 'pointer'
|
||||||
|
}}
|
||||||
|
onClick={() => handleDocumentClick(document)}
|
||||||
|
title={`Click to open ${document.name}`}
|
||||||
|
>
|
||||||
|
<span style={{ fontSize: '16px' }}>
|
||||||
|
{getFileIcon(document.type, document.ext)}
|
||||||
|
</span>
|
||||||
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
|
<div style={{
|
||||||
|
fontSize: '14px',
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap'
|
||||||
|
}}>
|
||||||
|
{document.ext ? `${document.name}.${document.ext}` : document.name}
|
||||||
|
</div>
|
||||||
{document.size && (
|
{document.size && (
|
||||||
<span className={styles.document_size}>
|
<div style={{
|
||||||
|
fontSize: '12px',
|
||||||
|
color: 'var(--color-gray)'
|
||||||
|
}}>
|
||||||
{formatFileSize(document.size)}
|
{formatFileSize(document.size)}
|
||||||
</span>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<div style={{ display: 'flex', gap: '4px' }}>
|
||||||
|
<button
|
||||||
|
onClick={(e) => handlePreview(document, e)}
|
||||||
|
style={{
|
||||||
|
padding: '4px 8px',
|
||||||
|
fontSize: '12px',
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
border: '1px solid var(--color-gray-disabled)',
|
||||||
|
borderRadius: '4px',
|
||||||
|
cursor: 'pointer'
|
||||||
|
}}
|
||||||
|
title="Preview file"
|
||||||
|
>
|
||||||
|
👁️
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={(e) => handleDownload(document, e)}
|
||||||
|
disabled={isDownloading}
|
||||||
|
style={{
|
||||||
|
padding: '4px 8px',
|
||||||
|
fontSize: '12px',
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
border: '1px solid var(--color-gray-disabled)',
|
||||||
|
borderRadius: '4px',
|
||||||
|
cursor: 'pointer'
|
||||||
|
}}
|
||||||
|
title="Download file"
|
||||||
|
>
|
||||||
|
⬇️
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.document_actions}>
|
);
|
||||||
<button
|
})}
|
||||||
className={styles.document_action_button}
|
|
||||||
onClick={(e) => handlePreview(document, e)}
|
|
||||||
title={t('chat.preview_document')}
|
|
||||||
>
|
|
||||||
<MdOutlineRemoveRedEye />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className={styles.document_action_button}
|
|
||||||
onClick={(e) => handleDownload(document, e)}
|
|
||||||
title={t('chat.download_document')}
|
|
||||||
>
|
|
||||||
<FaDownload />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
{message.timestamp && (
|
|
||||||
<div className={styles.message_timestamp}>
|
|
||||||
{new Date(message.timestamp).toLocaleTimeString()}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* File Preview Popup */}
|
|
||||||
{previewDocument && (
|
|
||||||
<FilePreviewPopup
|
|
||||||
document={previewDocument}
|
|
||||||
isOpen={isPreviewOpen}
|
|
||||||
onClose={handleClosePreview}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,83 +1,255 @@
|
||||||
import React from "react";
|
import React, { useEffect, useRef, useState, useCallback } from 'react';
|
||||||
import { motion } from "framer-motion";
|
import { useWorkflowStatus } from '../../../../hooks/useWorkflows';
|
||||||
import { MessageListProps } from "./dashboardChatAreaTypes";
|
import { Prompt } from '../../../../hooks/usePrompts';
|
||||||
import MessageItem from "./DashboardChatAreaMessageItem";
|
import { useApiRequest } from '../../../../hooks/useApi';
|
||||||
import WorkflowStatusDisplay from "./DashbaordChatAreaStatusDisplay";
|
import MessageItem from './DashboardChatAreaMessageItem';
|
||||||
import { useLanguage } from "../../../../contexts/LanguageContext";
|
import { Message, Document, WorkflowMessage } from './dashboardChatAreaTypes';
|
||||||
import styles from './DashboardChatArea.module.css';
|
|
||||||
|
|
||||||
const MessageList: React.FC<MessageListProps> = ({
|
interface MessageListProps {
|
||||||
messages,
|
selectedPrompt?: Prompt | null;
|
||||||
currentWorkflowId,
|
onPromptUsed?: () => void;
|
||||||
workflowStatus,
|
resumeWorkflowId?: string | null;
|
||||||
workflowCompleted,
|
onFilePreview?: (file: any) => void;
|
||||||
startingWorkflow,
|
}
|
||||||
startError,
|
|
||||||
messagesError,
|
// Custom hook to fetch and transform messages like the old code
|
||||||
messagesLoading,
|
const useTransformedMessages = (workflowId: string | null) => {
|
||||||
onStartNewWorkflow,
|
const [messages, setMessages] = useState<Message[]>([]);
|
||||||
messagesEndRef,
|
const [loading, setLoading] = useState(false);
|
||||||
handleRetry,
|
const [error, setError] = useState<string | null>(null);
|
||||||
shouldShowRetryButton
|
const { request } = useApiRequest();
|
||||||
|
|
||||||
|
const fetchMessages = useCallback(async () => {
|
||||||
|
if (!workflowId) {
|
||||||
|
setMessages([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log(`🔍 Fetching messages for workflow: ${workflowId}`);
|
||||||
|
|
||||||
|
// Fetch workflow messages
|
||||||
|
const workflowMessages: WorkflowMessage[] = await request({
|
||||||
|
url: `/api/workflows/${workflowId}/messages`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`📨 Received ${workflowMessages.length} messages from API:`, workflowMessages);
|
||||||
|
|
||||||
|
// Debug each message structure
|
||||||
|
workflowMessages.forEach((msg, index) => {
|
||||||
|
console.log(`📄 Message ${index + 1}:`, {
|
||||||
|
id: msg.id,
|
||||||
|
role: msg.role,
|
||||||
|
content: msg.content?.substring(0, 50) + '...',
|
||||||
|
fileIds: msg.fileIds,
|
||||||
|
hasFileIds: !!(msg.fileIds && msg.fileIds.length > 0),
|
||||||
|
fileIdsLength: msg.fileIds?.length || 0
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Transform each message
|
||||||
|
const transformedMessages = await Promise.all(
|
||||||
|
workflowMessages.map(async (workflowMessage: WorkflowMessage, msgIndex) => {
|
||||||
|
console.log(`🔄 Transforming message ${msgIndex + 1} (${workflowMessage.id})`);
|
||||||
|
let documents: Document[] = [];
|
||||||
|
|
||||||
|
// Fetch file metadata if fileIds exist
|
||||||
|
if (workflowMessage.fileIds && workflowMessage.fileIds.length > 0) {
|
||||||
|
console.log(`📎 Message ${workflowMessage.id} has ${workflowMessage.fileIds.length} fileIds:`, workflowMessage.fileIds);
|
||||||
|
|
||||||
|
const documentPromises = workflowMessage.fileIds.map(async (fileId, fileIndex) => {
|
||||||
|
try {
|
||||||
|
console.log(`📁 Fetching metadata for file ${fileIndex + 1}/${workflowMessage.fileIds!.length}: ${fileId}`);
|
||||||
|
const response = await request({
|
||||||
|
url: `/api/workflows/files/${fileId}/preview`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`✅ File ${fileId} metadata received:`, response);
|
||||||
|
|
||||||
|
const document: Document = {
|
||||||
|
id: fileId.toString(),
|
||||||
|
fileId: fileId,
|
||||||
|
name: response.name || response.fileName || `File_${fileId}`,
|
||||||
|
ext: response.extension || response.ext || (response.name ? response.name.split('.').pop() : 'txt'),
|
||||||
|
type: response.mimeType || response.type || 'application/octet-stream',
|
||||||
|
size: response.size || 0,
|
||||||
|
downloadUrl: response.downloadUrl || response.url
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(`🗂️ Created document object:`, document);
|
||||||
|
return document;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ Failed to fetch metadata for file ${fileId}:`, error);
|
||||||
|
// Return a fallback object for failed requests
|
||||||
|
const fallbackDoc: Document = {
|
||||||
|
id: fileId.toString(),
|
||||||
|
fileId: fileId,
|
||||||
|
name: `File_${fileId}`,
|
||||||
|
ext: 'unknown',
|
||||||
|
type: 'application/octet-stream',
|
||||||
|
size: 0
|
||||||
|
};
|
||||||
|
console.log(`🔧 Created fallback document:`, fallbackDoc);
|
||||||
|
return fallbackDoc;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
documents = await Promise.all(documentPromises);
|
||||||
|
console.log(`📋 All files processed for message ${workflowMessage.id}. Total documents: ${documents.length}`);
|
||||||
|
} else {
|
||||||
|
console.log(`📭 Message ${workflowMessage.id} has no fileIds`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transform to old Message format
|
||||||
|
const message: Message = {
|
||||||
|
id: workflowMessage.id,
|
||||||
|
role: workflowMessage.role,
|
||||||
|
agentName: workflowMessage.role === 'user' ? 'You' : 'Assistant',
|
||||||
|
content: workflowMessage.content,
|
||||||
|
timestamp: workflowMessage.timestamp,
|
||||||
|
documents: documents
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(`✨ Final transformed message:`, {
|
||||||
|
id: message.id,
|
||||||
|
role: message.role,
|
||||||
|
documentsCount: message.documents?.length || 0,
|
||||||
|
hasDocuments: !!(message.documents && message.documents.length > 0)
|
||||||
|
});
|
||||||
|
|
||||||
|
return message;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`🎉 Successfully transformed all ${transformedMessages.length} messages`);
|
||||||
|
console.log(`📊 Summary:`, transformedMessages.map(msg => ({
|
||||||
|
id: msg.id,
|
||||||
|
role: msg.role,
|
||||||
|
documentsCount: msg.documents?.length || 0
|
||||||
|
})));
|
||||||
|
|
||||||
|
setMessages(transformedMessages);
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('💥 Error fetching messages:', err);
|
||||||
|
setError(err.message || 'Failed to fetch messages');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [workflowId, request]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchMessages();
|
||||||
|
}, [fetchMessages]);
|
||||||
|
|
||||||
|
return { messages, loading, error, refetch: fetchMessages };
|
||||||
|
};
|
||||||
|
|
||||||
|
const MessageList: React.FC<MessageListProps> = ({
|
||||||
|
selectedPrompt,
|
||||||
|
onPromptUsed,
|
||||||
|
resumeWorkflowId,
|
||||||
|
onFilePreview
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useLanguage();
|
const { messages, loading, error, refetch } = useTransformedMessages(resumeWorkflowId || null);
|
||||||
|
const { status } = useWorkflowStatus(resumeWorkflowId || null);
|
||||||
|
const intervalRef = useRef<number | null>(null);
|
||||||
|
const [isInitialLoad, setIsInitialLoad] = React.useState(true);
|
||||||
|
|
||||||
|
// Auto-refresh messages every 3 seconds when workflow is active
|
||||||
|
useEffect(() => {
|
||||||
|
if (resumeWorkflowId && status?.status &&
|
||||||
|
(status.status === 'running' || status.status === 'processing' || status.status === 'started')) {
|
||||||
|
intervalRef.current = window.setInterval(() => {
|
||||||
|
console.log('🔄 Auto-refreshing messages due to active workflow');
|
||||||
|
refetch();
|
||||||
|
}, 3000);
|
||||||
|
} else {
|
||||||
|
// Stop polling when completed, failed, or no workflow
|
||||||
|
if (intervalRef.current) {
|
||||||
|
clearInterval(intervalRef.current);
|
||||||
|
intervalRef.current = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (intervalRef.current) {
|
||||||
|
clearInterval(intervalRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [resumeWorkflowId, status?.status, refetch]);
|
||||||
|
|
||||||
|
// Initial load when workflow ID changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (resumeWorkflowId) {
|
||||||
|
console.log(`🚀 Starting initial load for workflow: ${resumeWorkflowId}`);
|
||||||
|
setIsInitialLoad(true);
|
||||||
|
refetch().finally(() => setIsInitialLoad(false));
|
||||||
|
} else {
|
||||||
|
setIsInitialLoad(false);
|
||||||
|
}
|
||||||
|
}, [resumeWorkflowId, refetch]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<div style={{ padding: '16px', height: '100%', overflow: 'auto' }}>
|
||||||
className={styles.chat_messages}
|
<h3>Messages</h3>
|
||||||
initial={{ opacity: 0 }}
|
|
||||||
animate={{ opacity: 1 }}
|
{error && <p style={{ color: 'red' }}>Error: {error}</p>}
|
||||||
transition={{ delay: 0.2, duration: 0.3, ease: "easeOut" }}
|
|
||||||
>
|
{status && (
|
||||||
<div className={styles.messages_container}>
|
<div style={{
|
||||||
{startingWorkflow && (
|
padding: '8px',
|
||||||
<div className={styles.loading_message}>
|
backgroundColor: 'var(--color-surface)',
|
||||||
<p>{workflowCompleted && currentWorkflowId ? t('chat.sending_followup', 'Sending follow-up message...') : t('chat.sending_message', 'Sending message...')}</p>
|
borderRadius: '4px',
|
||||||
</div>
|
marginBottom: '16px'
|
||||||
)}
|
}}>
|
||||||
{startError && (
|
<strong>Status:</strong> {status.status}
|
||||||
<div className={styles.error_message}>
|
{status.currentRound && ` (Round ${status.currentRound})`}
|
||||||
<p>{t('chat.error_prefix', 'Error:')} {startError}</p>
|
{/* Show a small indicator when polling for updates */}
|
||||||
</div>
|
{intervalRef.current && (
|
||||||
)}
|
<span style={{
|
||||||
{messagesError && (
|
marginLeft: '8px',
|
||||||
<div className={styles.error_message}>
|
fontSize: '12px',
|
||||||
<p>{t('chat.error_loading_messages', 'Error loading messages:')} {messagesError}</p>
|
color: 'var(--color-secondary)',
|
||||||
</div>
|
opacity: 0.7
|
||||||
)}
|
}}>
|
||||||
{currentWorkflowId && messagesLoading && messages.length === 0 && (
|
🔄 Live updates
|
||||||
<div className={styles.loading_message}>
|
</span>
|
||||||
<p>{t('chat.loading_workflow_messages', 'Loading workflow messages...')}</p>
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{messages.length > 0 ? (
|
|
||||||
messages.map((message, index) => (
|
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||||
|
{messages.map((message, index) => {
|
||||||
|
console.log(`🎨 Rendering message ${message.id} with ${message.documents?.length || 0} documents`);
|
||||||
|
return (
|
||||||
<MessageItem
|
<MessageItem
|
||||||
key={message.id || index}
|
key={message.id}
|
||||||
message={message}
|
message={message}
|
||||||
index={index}
|
index={index}
|
||||||
|
onFilePreview={onFilePreview}
|
||||||
/>
|
/>
|
||||||
))
|
);
|
||||||
) : !currentWorkflowId ? (
|
})}
|
||||||
<p className={styles.placeholder_text}>{t('chat.start_conversation', 'Start a conversation by entering a message, selecting a template, or continuing a previous workflow...')}</p>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{/* Spacer to push workflow status to bottom when there are fewer messages */}
|
|
||||||
{messages.length < 3 && <div className={styles.messages_spacer} />}
|
|
||||||
|
|
||||||
<WorkflowStatusDisplay
|
|
||||||
currentWorkflowId={currentWorkflowId}
|
|
||||||
workflowStatus={workflowStatus}
|
|
||||||
workflowCompleted={workflowCompleted}
|
|
||||||
onStartNewWorkflow={onStartNewWorkflow}
|
|
||||||
handleRetry={handleRetry}
|
|
||||||
shouldShowRetryButton={shouldShowRetryButton}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div ref={messagesEndRef} />
|
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
|
||||||
|
{loading && (
|
||||||
|
<div style={{ textAlign: 'center', padding: '16px' }}>
|
||||||
|
<p style={{ color: 'var(--color-gray)' }}>Loading messages...</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{messages.length === 0 && !isInitialLoad && !loading && (
|
||||||
|
<p style={{ color: 'var(--color-gray)', textAlign: 'center' }}>
|
||||||
|
No messages yet. Start a workflow to see messages here.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default MessageList;
|
export default MessageList;
|
||||||
|
|
|
||||||
|
|
@ -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 { Prompt } from "../../../../hooks/usePrompts";
|
||||||
import { FileInfo } from "../../../../hooks/useFiles";
|
|
||||||
|
|
||||||
export interface DashboardChatAreaProps {
|
export interface DashboardChatAreaProps {
|
||||||
selectedPrompt?: Prompt | null;
|
selectedPrompt?: Prompt | null;
|
||||||
|
|
@ -9,6 +8,26 @@ export interface DashboardChatAreaProps {
|
||||||
resumeWorkflowId?: string | null;
|
resumeWorkflowId?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface WorkflowMessage {
|
||||||
|
id: string;
|
||||||
|
role: 'user' | 'assistant' | 'system';
|
||||||
|
content: string;
|
||||||
|
timestamp?: string;
|
||||||
|
fileIds?: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FileInfo {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
mimeType: string;
|
||||||
|
size?: number;
|
||||||
|
creationDate?: string;
|
||||||
|
downloadUrl?: string;
|
||||||
|
fileData?: File;
|
||||||
|
objectUrl?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add old interfaces for compatibility
|
||||||
export interface Document {
|
export interface Document {
|
||||||
id?: string;
|
id?: string;
|
||||||
fileId?: number;
|
fileId?: number;
|
||||||
|
|
@ -27,50 +46,4 @@ export interface Message {
|
||||||
content: string;
|
content: string;
|
||||||
timestamp?: string;
|
timestamp?: string;
|
||||||
documents?: Document[];
|
documents?: Document[];
|
||||||
}
|
|
||||||
|
|
||||||
export interface WorkflowStatus {
|
|
||||||
status: string;
|
|
||||||
currentRound?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ChatInputProps {
|
|
||||||
inputValue: string;
|
|
||||||
setInputValue: (value: string) => void;
|
|
||||||
onSend: () => void;
|
|
||||||
onKeyPress: (e: React.KeyboardEvent) => void;
|
|
||||||
isDisabled: boolean;
|
|
||||||
placeholder: string;
|
|
||||||
inputRef: React.RefObject<HTMLTextAreaElement | null>;
|
|
||||||
isWorkflowRunning: boolean;
|
|
||||||
onStopWorkflow: () => void;
|
|
||||||
isStoppingWorkflow: boolean;
|
|
||||||
attachedFiles: FileInfo[];
|
|
||||||
onFileAttach: (file: File) => void;
|
|
||||||
onFileRemove: (fileId: number) => void;
|
|
||||||
onFilesSelect: (files: FileInfo[]) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MessageListProps {
|
|
||||||
messages: Message[];
|
|
||||||
currentWorkflowId: string | null;
|
|
||||||
workflowStatus: WorkflowStatus | null;
|
|
||||||
workflowCompleted: boolean;
|
|
||||||
startingWorkflow: boolean;
|
|
||||||
startError: string | null;
|
|
||||||
messagesError: string | null;
|
|
||||||
messagesLoading: boolean;
|
|
||||||
onStartNewWorkflow: () => void;
|
|
||||||
messagesEndRef: React.RefObject<HTMLDivElement | null>;
|
|
||||||
handleRetry: () => Promise<void>;
|
|
||||||
shouldShowRetryButton: () => boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WorkflowStatusDisplayProps {
|
|
||||||
currentWorkflowId: string | null;
|
|
||||||
workflowStatus: WorkflowStatus | null;
|
|
||||||
workflowCompleted: boolean;
|
|
||||||
onStartNewWorkflow: () => void;
|
|
||||||
handleRetry: () => Promise<void>;
|
|
||||||
shouldShowRetryButton: () => boolean;
|
|
||||||
}
|
}
|
||||||
|
|
@ -3,7 +3,7 @@ import { MdOutlineRemoveRedEye } from "react-icons/md";
|
||||||
import styles from "./DateienItem.module.css";
|
import styles from "./DateienItem.module.css";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useFileOperations } from "../../hooks/useFiles";
|
import { useFileOperations } from "../../hooks/useFiles";
|
||||||
import FilePreviewPopup from "../Dashboard/DashboardChat/DashboardChatArea/FilePreviewPopup";
|
import FilePreviewPopup from "../Dashboard/DashboardChat/DashboardChatArea/DashboardChatAreaFilePreview";
|
||||||
import { Document } from "../Dashboard/DashboardChat/DashboardChatArea/dashboardChatAreaTypes";
|
import { Document } from "../Dashboard/DashboardChat/DashboardChatArea/dashboardChatAreaTypes";
|
||||||
import { useLanguage } from "../../contexts/LanguageContext";
|
import { useLanguage } from "../../contexts/LanguageContext";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,28 @@
|
||||||
/* Allgemeine Stile */
|
/* Allgemeine Stile */
|
||||||
.sidebarContainer {
|
.sidebarContainer {
|
||||||
border-radius: 30px;
|
border-radius: 0px;
|
||||||
border: none;
|
border-right: 1px solid var(--color-primary);
|
||||||
background: var(--color-bg);
|
background: var(--color-bg);
|
||||||
box-shadow: 0px 2px 6px 0px rgba(194, 194, 194, 0.10);
|
box-shadow: 0px 2px 6px 0px rgba(194, 194, 194, 0.10);
|
||||||
width: 240px;
|
width: 240px;
|
||||||
margin-top: 51px;
|
|
||||||
margin-left: 49px;
|
|
||||||
padding-bottom: 1px;
|
padding-bottom: 1px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: top;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar {
|
.sidebar {
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 240px;
|
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
margin: 0 0 30px 0;
|
|
||||||
font-family: var(--font-family);
|
font-family: var(--font-family);
|
||||||
}
|
}
|
||||||
|
|
||||||
.logoContainer {
|
.logoContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 100%;
|
|
||||||
height: 80px; /* Fixed height instead of auto */
|
height: 80px; /* Fixed height instead of auto */
|
||||||
padding: 30px 20px 7px 20px;
|
padding: 30px 20px 7px 20px;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
@ -38,7 +34,7 @@
|
||||||
|
|
||||||
.logoWrapper {
|
.logoWrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: left;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
@ -49,24 +45,42 @@
|
||||||
color: var(--color-primary);
|
color: var(--color-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Toggle Button Styles */
|
.logoText {
|
||||||
.toggleButton {
|
font-family: var(--font-family);
|
||||||
background: var(--color-primary);
|
font-size: 35px;
|
||||||
border: none;
|
|
||||||
border-radius: 10px;
|
|
||||||
color: white;
|
|
||||||
width: 30px;
|
|
||||||
height: 30px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
letter-spacing: -0.5px;
|
||||||
|
font-weight: 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logoPower {
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logoOn {
|
||||||
|
color: var(--color-secondary);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Toggle Button Styles */
|
||||||
|
.toggleButton {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
color: var(--color-text);
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: right;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toggleButton:hover {
|
.toggleButton:hover {
|
||||||
background: var(--color-primary-hover);
|
background:none;
|
||||||
transform: scale(1.05);
|
transform: scale(1.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -74,7 +88,7 @@
|
||||||
.sidebarContainer.minimized {
|
.sidebarContainer.minimized {
|
||||||
width: 80px;
|
width: 80px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: top;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -93,7 +107,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebarContainer.minimized .toggleButton {
|
.sidebarContainer.minimized .toggleButton {
|
||||||
margin: 0 auto; /* Center the toggle button */
|
margin: 0 auto;
|
||||||
|
justify-content: center;
|
||||||
|
/* Center the toggle button */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,10 @@ const Sidebar: React.FC<SidebarProps> = ({ data }) => {
|
||||||
exit={{ opacity: 0, scale: 0.8 }}
|
exit={{ opacity: 0, scale: 0.8 }}
|
||||||
transition={{ duration: 0.2 }}
|
transition={{ duration: 0.2 }}
|
||||||
>
|
>
|
||||||
<img src="/logos/PowerOn_transparent.png" alt="Logo" className={styles.logo} />
|
<div className={styles.logoText}>
|
||||||
|
<span className={styles.logoPower}>Power</span>
|
||||||
|
<span className={styles.logoOn}>On</span>
|
||||||
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
|
|
||||||
.menu li {
|
.menu li {
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 200px;
|
width: 220px;
|
||||||
height: 44px;
|
height: 44px;
|
||||||
padding: 0 3px 0 15px;
|
padding: 0 3px 0 15px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -55,7 +55,6 @@
|
||||||
padding: 2.292px 2.3px 2.508px 2.292px;
|
padding: 2.292px 2.3px 2.508px 2.292px;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-left: 20px;
|
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,12 @@
|
||||||
.user_section {
|
.user_section {
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 240px;
|
width: 240px;
|
||||||
height: 100px; /* Fixed height instead of auto */
|
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-start;
|
align-items: left;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
font-family: var(--font-family);
|
font-family: var(--font-family);
|
||||||
box-sizing: border-box; /* Include padding in height calculation */
|
box-sizing: border-box;
|
||||||
margin-bottom: 30px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.user_info {
|
.user_info {
|
||||||
|
|
@ -37,9 +35,10 @@
|
||||||
.user_section h1 {
|
.user_section h1 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 16pt;
|
font-size: 16pt;
|
||||||
line-height: 1.2;
|
line-height: 1.;
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
font-family: var(--font-family);
|
font-family: var(--font-family);
|
||||||
|
font-weight: 400;
|
||||||
transition: opacity 0.3s ease;
|
transition: opacity 0.3s ease;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
@ -85,7 +84,6 @@
|
||||||
/* Minimized User Section Styles */
|
/* Minimized User Section Styles */
|
||||||
.user_section.minimized {
|
.user_section.minimized {
|
||||||
width: 46px; /* Match menu item width */
|
width: 46px; /* Match menu item width */
|
||||||
height: 100px; /* Same fixed height as expanded */
|
|
||||||
padding: 20px 15px 20px 15px; /* Match menu item padding structure */
|
padding: 20px 15px 20px 15px; /* Match menu item padding structure */
|
||||||
align-items: center;
|
align-items: center;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|
|
||||||
|
|
@ -45,18 +45,11 @@ const SidebarUser: React.FC<SidebarUserProps> = ({ user, isLoading, error, isMin
|
||||||
return (
|
return (
|
||||||
<div className={`${styles.user_section} ${isMinimized ? styles.minimized : ''}`}>
|
<div className={`${styles.user_section} ${isMinimized ? styles.minimized : ''}`}>
|
||||||
<div className={styles.user_info}>
|
<div className={styles.user_info}>
|
||||||
<FaUserCircle className={styles.user_icon} />
|
|
||||||
<div className={styles.text_content}>
|
<div className={styles.text_content}>
|
||||||
<h1>{ user.name }</h1>
|
<h1>{ user.name }</h1>
|
||||||
<p>Rolle: {user.role}</p>
|
<p>Rolle: {user.role}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
|
||||||
className={styles.logout_button}
|
|
||||||
onClick={handleLogout}
|
|
||||||
>
|
|
||||||
<span className={styles.logout_text}>Logout</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,63 +1,24 @@
|
||||||
.dashboardContainer {
|
.dashboardContainer {
|
||||||
margin: 51px 49px 0 36px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
font-family: var(--font-family);
|
font-family: var(--font-family);
|
||||||
width: 98%;
|
width: 100%;
|
||||||
max-height: calc(100vh - 100px);
|
height: 100vh;
|
||||||
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chatLogContainer {
|
.chatLogContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 20px;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatLogContainer.expanded {
|
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 20px;
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Height classes for different states */
|
.chatArea {
|
||||||
.chatArea15vh {
|
display: flex;
|
||||||
height: 35vh;
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chatArea40vh {
|
|
||||||
height: 60vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatArea45vh {
|
|
||||||
height: 60vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chatArea60vh {
|
|
||||||
height: 85vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logArea15vh {
|
|
||||||
height: 10vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logArea25vh {
|
|
||||||
height: 25vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logArea40vh {
|
|
||||||
height: 60vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logArea60vh {
|
|
||||||
height: 85vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.promptArea30vh {
|
|
||||||
height: 30vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.promptArea40vh {
|
|
||||||
height: 40vh;
|
|
||||||
}
|
|
||||||
|
|
@ -42,58 +42,12 @@ function Dashboard () {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Determine CSS classes based on states
|
// Determine CSS classes based on states
|
||||||
const getPromptClass = () => {
|
|
||||||
if (isPromptAreaCollapsed) return '';
|
|
||||||
return isChatExpanded ? styles.promptArea40vh : styles.promptArea30vh;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getChatClass = () => {
|
|
||||||
if (isPromptAreaCollapsed && isChatExpanded) return styles.chatArea45vh;
|
|
||||||
if (!isPromptAreaCollapsed && isChatExpanded) return styles.chatArea15vh;
|
|
||||||
if (isPromptAreaCollapsed && !isChatExpanded) return styles.chatArea60vh;
|
|
||||||
return styles.chatArea40vh;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getLogClass = () => {
|
|
||||||
if (isPromptAreaCollapsed && isChatExpanded) return styles.logArea25vh;
|
|
||||||
if (!isPromptAreaCollapsed && isChatExpanded) return styles.logArea15vh;
|
|
||||||
if (isPromptAreaCollapsed && !isChatExpanded) return styles.logArea60vh;
|
|
||||||
return styles.logArea40vh;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Memoize style objects to prevent infinite re-renders
|
|
||||||
const promptStyle = useMemo(() => ({
|
|
||||||
marginBottom: !isPromptAreaCollapsed ? "0px" : "0"
|
|
||||||
}), [isPromptAreaCollapsed]);
|
|
||||||
|
|
||||||
const chatStyle = useMemo(() => ({
|
|
||||||
width: isChatExpanded ? "100%" : "calc(50% - 10px)",
|
|
||||||
flex: isChatExpanded ? "none" : "1",
|
|
||||||
marginBottom: isChatExpanded ? "0px" : "0"
|
|
||||||
}), [isChatExpanded]);
|
|
||||||
|
|
||||||
const logStyle = useMemo(() => ({
|
|
||||||
width: isChatExpanded ? "100%" : "calc(50% - 10px)",
|
|
||||||
flex: isChatExpanded ? "none" : "1"
|
|
||||||
}), [isChatExpanded]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.dashboardContainer}>
|
<div className={styles.dashboardContainer}>
|
||||||
<div
|
|
||||||
className={getPromptClass()}
|
|
||||||
style={promptStyle}
|
|
||||||
>
|
|
||||||
<DashboardPrompt
|
|
||||||
onPromptRun={handlePromptRun}
|
|
||||||
isCollapsed={isPromptAreaCollapsed}
|
|
||||||
onToggleCollapse={() => setIsPromptAreaCollapsed(!isPromptAreaCollapsed)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className={`${styles.chatLogContainer} ${isChatExpanded ? styles.expanded : ''}`}>
|
<div className={`${styles.chatLogContainer} ${isChatExpanded ? styles.expanded : ''}`}>
|
||||||
<div
|
<div className={styles.chatArea}>
|
||||||
className={getChatClass()}
|
|
||||||
style={chatStyle}
|
|
||||||
>
|
|
||||||
<DashboardChat
|
<DashboardChat
|
||||||
isExpanded={isChatExpanded}
|
isExpanded={isChatExpanded}
|
||||||
onToggleExpand={handleChatToggleExpand}
|
onToggleExpand={handleChatToggleExpand}
|
||||||
|
|
@ -104,16 +58,6 @@ function Dashboard () {
|
||||||
onWorkflowResume={handleWorkflowResume}
|
onWorkflowResume={handleWorkflowResume}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
className={getLogClass()}
|
|
||||||
style={logStyle}
|
|
||||||
>
|
|
||||||
<DashboardLog
|
|
||||||
isExpanded={isChatExpanded}
|
|
||||||
workflowId={currentWorkflowId}
|
|
||||||
workflowCompleted={workflowCompleted}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -59,14 +59,16 @@
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
background: var(--color-surface);
|
background: var(--color-bg);
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
|
border: 2px solid var(--color-surface);
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settingInfo {
|
.settingInfo {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
color: var(--color-text);
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
@ -80,7 +82,7 @@
|
||||||
|
|
||||||
.settingDescription {
|
.settingDescription {
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
color: var(--color-gray);
|
color: var(--color-primary);
|
||||||
font-family: var(--font-family);
|
font-family: var(--font-family);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -90,7 +92,7 @@
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
padding: 12px 20px;
|
padding: 12px 20px;
|
||||||
border-radius: 25px;
|
border-radius: 25px;
|
||||||
border: 2px solid var(--color-gray-disabled);
|
border: 2px solid var(--color-primary);
|
||||||
background: var(--color-bg);
|
background: var(--color-bg);
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
@ -107,19 +109,6 @@
|
||||||
box-shadow: 0 4px 12px rgba(63, 81, 181, 0.15);
|
box-shadow: 0 4px 12px rgba(63, 81, 181, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
.themeToggle.light {
|
|
||||||
background: linear-gradient(135deg, var(--color-bg) 0%, var(--color-surface) 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.themeToggle.dark {
|
|
||||||
background: linear-gradient(135deg, var(--color-surface) 0%, var(--color-bg) 100%);
|
|
||||||
border-color: var(--color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.themeToggle.dark:hover {
|
|
||||||
border-color: var(--color-primary-hover);
|
|
||||||
box-shadow: 0 4px 12px rgba(178, 102, 255, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
.toggleSlider {
|
.toggleSlider {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@
|
||||||
font-family: var(--font-family);
|
font-family: var(--font-family);
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding: 0 49px 0 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.homeContainer::before {
|
.homeContainer::before {
|
||||||
|
|
@ -22,16 +21,21 @@
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.homeSidebar {
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
.homeContent {
|
|
||||||
height: 100vh;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.body {
|
.body {
|
||||||
display: flex;
|
display: flex;
|
||||||
max-width: 100vw;
|
max-width: 100vw;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.homeSidebar {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.homeContent {
|
||||||
|
height: 100vh;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
@ -25,6 +25,7 @@ function Home () {
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
transition={{ duration: 0.3, ease: "easeInOut" }}
|
transition={{ duration: 0.3, ease: "easeInOut" }}
|
||||||
|
style={{ height: "100%", display: "flex", flexDirection: "column" }}
|
||||||
>
|
>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue