integrated workflows
This commit is contained in:
parent
4db660d247
commit
bc65e5a112
28 changed files with 3705 additions and 477 deletions
903
package-lock.json
generated
903
package-lock.json
generated
File diff suppressed because it is too large
Load diff
12
package.json
12
package.json
|
|
@ -10,8 +10,8 @@
|
|||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@azure/msal-browser": "^4.8.0",
|
||||
"@azure/msal-react": "^3.0.7",
|
||||
"@azure/msal-browser": "^4.12.0",
|
||||
"@azure/msal-react": "^3.0.12",
|
||||
"axios": "^1.8.3",
|
||||
"dotenv": "^16.0.3",
|
||||
"framer-motion": "^12.7.3",
|
||||
|
|
@ -21,16 +21,16 @@
|
|||
"jwt-decode": "^4.0.0",
|
||||
"motion": "^12.7.3",
|
||||
"pg": "^8.8.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-dropzone": "^14.3.8",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-router-dom": "^7.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.21.0",
|
||||
"@types/react": "^19.0.10",
|
||||
"@types/react-dom": "^19.0.4",
|
||||
"@types/react": "^19.1.6",
|
||||
"@types/react-dom": "^19.1.5",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"eslint": "^9.21.0",
|
||||
"eslint-plugin-react-hooks": "^5.1.0",
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { ProtectedRoute } from './auth/ProtectedRoute';
|
|||
import Home from './pages/Home';
|
||||
import Dateien from './pages/Dateien/Dateien';
|
||||
import Mitglieder from './pages/Mitglieder/Mitglieder';
|
||||
import Dashboard from './pages/Dashboard';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
|
|
@ -22,6 +23,7 @@ function App() {
|
|||
<Home />
|
||||
</ProtectedRoute>
|
||||
}>
|
||||
<Route path="dashboard" element={<Dashboard />} />
|
||||
<Route path="dateien" element={<Dateien />} />
|
||||
<Route path="mitglieder" element={<Mitglieder />} />
|
||||
</Route>
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@
|
|||
import axios from 'axios';
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: 'https://gateway.poweron-center.net',
|
||||
/*baseURL: 'http://localhost:8000',*/
|
||||
//baseURL: 'https://gateway.poweron-center.net',
|
||||
baseURL: 'http://localhost:8000',
|
||||
withCredentials: true
|
||||
});
|
||||
|
||||
|
|
|
|||
304
src/components/Dashboard/DashboardChat/DashboardChat.module.css
Normal file
304
src/components/Dashboard/DashboardChat/DashboardChat.module.css
Normal file
|
|
@ -0,0 +1,304 @@
|
|||
.dashboard_chat {
|
||||
display: flex;
|
||||
padding: 20px;
|
||||
flex-direction: column;
|
||||
align-self: stretch;
|
||||
border-radius: 30px;
|
||||
border: 1px solid var(--f-1-f-1-f-1, #F1F1F1);
|
||||
background: var(--Grayscale-True-White, #FFF);
|
||||
position: relative;
|
||||
box-shadow: 0px 2px 6px 0px rgba(194, 194, 194, 0.10);
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.chat_button_active {
|
||||
color: var(--Grayscale-Black, #24262B);
|
||||
}
|
||||
|
||||
.chat_button_inactive {
|
||||
color: #A0A0A0;
|
||||
}
|
||||
|
||||
.chat_button_collapsed {
|
||||
opacity: 50%;
|
||||
color: #A0A0A0;
|
||||
}
|
||||
|
||||
.iconContainer {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.expandIcon, .collapseIcon {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--Brand-Green-Green, #3A8088);
|
||||
}
|
||||
|
||||
.expandIcon:hover, .collapseIcon:hover {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.horizontalLine {
|
||||
width: 100%;
|
||||
background-color: black;
|
||||
height: 2px;
|
||||
margin-top: 19px;
|
||||
}
|
||||
|
||||
.horizontalLineLight {
|
||||
width: calc(100%);
|
||||
background-color: #F1F1F1;
|
||||
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: 1px solid #E0E0E0;
|
||||
border-radius: 12px;
|
||||
outline: none;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.message_input:focus {
|
||||
border-color: #666;
|
||||
}
|
||||
|
||||
.send_button {
|
||||
padding: 12px 12px;
|
||||
background-color: var(--Brand-Green-Green, #3A8088);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.send_button_icon {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
margin: none;
|
||||
padding: none;
|
||||
}
|
||||
|
||||
.send_button:disabled {
|
||||
background-color: #ccc;
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.message_input:disabled {
|
||||
background-color: #f5f5f5;
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.loading_message {
|
||||
padding: 10px;
|
||||
background-color: #e3f2fd;
|
||||
border-left: 4px solid #2196f3;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.loading_message p {
|
||||
margin: 0;
|
||||
color: #1976d2;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.error_message {
|
||||
padding: 10px;
|
||||
background-color: #ffebee;
|
||||
border-left: 4px solid #f44336;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.error_message p {
|
||||
margin: 0;
|
||||
color: #c62828;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.message {
|
||||
margin-bottom: 15px;
|
||||
padding: 12px;
|
||||
border-radius: 12px;
|
||||
max-width: 80%;
|
||||
}
|
||||
|
||||
.message_user {
|
||||
background-color: var(--Brand-Green-Green, #3A8088);
|
||||
color: white;
|
||||
margin-left: auto;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.message_assistant {
|
||||
background-color: #f5f5f5;
|
||||
color: #333;
|
||||
margin-left: 0;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.message_system {
|
||||
background-color: #fff3cd;
|
||||
color: #856404;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.message_role {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.message_content {
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.message_timestamp {
|
||||
font-size: 11px;
|
||||
margin-top: 4px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.placeholder_text {
|
||||
text-align: center;
|
||||
color: #999;
|
||||
font-style: italic;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.workflow_status {
|
||||
padding: 8px 12px;
|
||||
background-color: #e8f5e8;
|
||||
border-left: 4px solid #4caf50;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.workflow_status p {
|
||||
margin: 0;
|
||||
color: #2e7d32;
|
||||
font-size: 13px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.completion_message {
|
||||
padding: 10px 12px;
|
||||
background-color: #e8f5e8;
|
||||
border-left: 4px solid #4caf50;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.completion_message p {
|
||||
margin: 0 0 10px 0;
|
||||
color: #2e7d32;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.new_workflow_button {
|
||||
background-color: var(--Brand-Green-Green, #3A8088);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 8px 16px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.new_workflow_button:hover {
|
||||
background-color: #2d6b73;
|
||||
}
|
||||
|
||||
134
src/components/Dashboard/DashboardChat/DashboardChat.tsx
Normal file
134
src/components/Dashboard/DashboardChat/DashboardChat.tsx
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
import React, { useState } from "react";
|
||||
import { BsArrowsAngleExpand, BsArrowsAngleContract } from "react-icons/bs";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { Prompt } from "../../../hooks/usePrompts";
|
||||
|
||||
import DashboardChatArea from './DashboardChatArea/DashboardChatArea';
|
||||
import DashboardChatHistory from './DashboardChatHistory/DashboardChatHistory';
|
||||
|
||||
import styles from './DashboardChat.module.css';
|
||||
|
||||
interface DashboardChatProps {
|
||||
isExpanded: boolean;
|
||||
onToggleExpand: () => void;
|
||||
selectedPrompt?: Prompt | null;
|
||||
onPromptUsed?: () => void;
|
||||
onWorkflowIdChange?: (workflowId: string | null) => void;
|
||||
onWorkflowResume?: (workflowId: string) => void;
|
||||
}
|
||||
|
||||
const DashboardChat: React.FC<DashboardChatProps> = ({
|
||||
isExpanded,
|
||||
onToggleExpand,
|
||||
selectedPrompt,
|
||||
onPromptUsed,
|
||||
onWorkflowIdChange,
|
||||
onWorkflowResume
|
||||
}) => {
|
||||
const [activeTab, setActiveTab] = useState("Chat Area");
|
||||
const [resumeWorkflowId, setResumeWorkflowId] = useState<string | null>(null);
|
||||
|
||||
const handleWorkflowResume = (workflowId: string) => {
|
||||
// Switch to Chat Area tab first
|
||||
setActiveTab("Chat Area");
|
||||
// Set the workflow ID to resume
|
||||
setResumeWorkflowId(workflowId);
|
||||
// Then call the parent's resume handler
|
||||
if (onWorkflowResume) {
|
||||
onWorkflowResume(workflowId);
|
||||
}
|
||||
// Clear the resume ID after a short delay to allow processing
|
||||
setTimeout(() => {
|
||||
setResumeWorkflowId(null);
|
||||
}, 100);
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
className={`${styles.dashboard_chat} ${isExpanded ? styles.expanded : ''}`}
|
||||
layout
|
||||
transition={{ duration: 0.4, ease: "easeOut" }}
|
||||
>
|
||||
<motion.div
|
||||
className={styles.chat_header}
|
||||
layout
|
||||
transition={{ duration: 0.3, ease: "easeOut" }}
|
||||
>
|
||||
<div className={styles.chat_button_div}>
|
||||
{["Chat Area", "Workflow 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 === "Chat Area" ? (
|
||||
<DashboardChatArea
|
||||
selectedPrompt={selectedPrompt}
|
||||
onPromptUsed={onPromptUsed}
|
||||
onWorkflowIdChange={onWorkflowIdChange}
|
||||
resumeWorkflowId={resumeWorkflowId}
|
||||
/>
|
||||
) : (
|
||||
<DashboardChatHistory
|
||||
onWorkflowResume={handleWorkflowResume}
|
||||
/>
|
||||
)}
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardChat;
|
||||
|
|
@ -0,0 +1,226 @@
|
|||
.chat_area {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.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: #ccc;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.chat_messages::-webkit-scrollbar-thumb:hover {
|
||||
background: #999;
|
||||
}
|
||||
|
||||
.messages_container {
|
||||
min-height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.chat_input {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.message_input {
|
||||
flex: 1;
|
||||
padding: 12px 16px;
|
||||
border: 1px solid #E0E0E0;
|
||||
border-radius: 12px;
|
||||
outline: none;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.message_input:focus {
|
||||
border-color: #666;
|
||||
}
|
||||
|
||||
.message_input:disabled {
|
||||
background-color: #f5f5f5;
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.send_button {
|
||||
padding: 12px 12px;
|
||||
background-color: var(--Brand-Green-Green, #3A8088);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.send_button_icon {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
margin: none;
|
||||
padding: none;
|
||||
}
|
||||
|
||||
.send_button:disabled {
|
||||
background-color: #ccc;
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.loading_message {
|
||||
padding: 10px;
|
||||
background-color: #e3f2fd;
|
||||
border-left: 4px solid #2196f3;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.loading_message p {
|
||||
margin: 0;
|
||||
color: #1976d2;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.error_message {
|
||||
padding: 10px;
|
||||
background-color: #ffebee;
|
||||
border-left: 4px solid #f44336;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.error_message p {
|
||||
margin: 0;
|
||||
color: #c62828;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.message {
|
||||
margin-bottom: 15px;
|
||||
padding: 12px;
|
||||
border-radius: 12px;
|
||||
max-width: 80%;
|
||||
}
|
||||
|
||||
.message_user {
|
||||
background-color: var(--Brand-Green-Green, #3A8088);
|
||||
color: white;
|
||||
margin-left: auto;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.message_assistant {
|
||||
background-color: #f5f5f5;
|
||||
color: #333;
|
||||
margin-left: 0;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.message_system {
|
||||
background-color: #fff3cd;
|
||||
color: #856404;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.message_role {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.message_content {
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.message_timestamp {
|
||||
font-size: 11px;
|
||||
margin-top: 4px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.placeholder_text {
|
||||
text-align: center;
|
||||
color: #999;
|
||||
font-style: italic;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.workflow_status {
|
||||
padding: 8px 12px;
|
||||
background-color: #e8f5e8;
|
||||
border-left: 4px solid #4caf50;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.workflow_status p {
|
||||
margin: 0;
|
||||
color: #2e7d32;
|
||||
font-size: 13px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.completion_message {
|
||||
padding: 10px 12px;
|
||||
background-color: #e8f5e8;
|
||||
border-left: 4px solid #4caf50;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.completion_message p {
|
||||
margin: 0 0 10px 0;
|
||||
color: #2e7d32;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.new_workflow_button {
|
||||
background-color: var(--Brand-Green-Green, #3A8088);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 8px 16px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.new_workflow_button:hover {
|
||||
background-color: #2d6b73;
|
||||
}
|
||||
|
|
@ -0,0 +1,278 @@
|
|||
import React, { useState, useEffect, useRef } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { LuSendHorizontal } from "react-icons/lu";
|
||||
import { Prompt } from "../../../../hooks/usePrompts";
|
||||
import { useWorkflowOperations, useWorkflowMessages, useWorkflowStatus } from "../../../../hooks/useWorkflows";
|
||||
|
||||
import styles from './DashboardChatArea.module.css';
|
||||
|
||||
interface DashboardChatAreaProps {
|
||||
selectedPrompt?: Prompt | null;
|
||||
onPromptUsed?: () => void;
|
||||
onWorkflowIdChange?: (workflowId: string | null) => void;
|
||||
resumeWorkflowId?: string | null;
|
||||
}
|
||||
|
||||
const DashboardChatArea: React.FC<DashboardChatAreaProps> = ({
|
||||
selectedPrompt,
|
||||
onPromptUsed,
|
||||
onWorkflowIdChange,
|
||||
resumeWorkflowId
|
||||
}) => {
|
||||
const [inputValue, setInputValue] = useState("");
|
||||
const [currentWorkflowId, setCurrentWorkflowId] = useState<string | null>(null);
|
||||
const [workflowCompleted, setWorkflowCompleted] = useState(false);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const { startWorkflow, startingWorkflow, startError } = useWorkflowOperations();
|
||||
const { messages, loading: messagesLoading, error: messagesError, refetch: refetchMessages } = useWorkflowMessages(currentWorkflowId);
|
||||
const { status: workflowStatus, refetch: refetchStatus } = useWorkflowStatus(currentWorkflowId);
|
||||
|
||||
// Update input value when a prompt is selected
|
||||
useEffect(() => {
|
||||
if (selectedPrompt) {
|
||||
setInputValue(selectedPrompt.content);
|
||||
// Focus the input field
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}
|
||||
}, [selectedPrompt]);
|
||||
|
||||
// Auto-scroll to bottom when new messages arrive
|
||||
useEffect(() => {
|
||||
if (messagesEndRef.current) {
|
||||
messagesEndRef.current.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
}, [messages]);
|
||||
|
||||
// Polling logic for fetching messages and status
|
||||
useEffect(() => {
|
||||
if (!currentWorkflowId || workflowCompleted) return;
|
||||
|
||||
const interval = setInterval(() => {
|
||||
refetchMessages();
|
||||
refetchStatus();
|
||||
}, 1000); // Poll every second
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [currentWorkflowId, workflowCompleted, refetchMessages, refetchStatus]);
|
||||
|
||||
// Check if workflow is completed based on status or messages
|
||||
useEffect(() => {
|
||||
if (workflowStatus && (
|
||||
workflowStatus.status === 'completed' ||
|
||||
workflowStatus.status === 'finished' ||
|
||||
workflowStatus.status === 'done' ||
|
||||
workflowStatus.status === 'stopped'
|
||||
)) {
|
||||
setWorkflowCompleted(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (messages.length > 0) {
|
||||
const lastMessage = messages[messages.length - 1];
|
||||
// Check if the last message indicates completion
|
||||
if (lastMessage.role === 'assistant' &&
|
||||
(lastMessage.content.toLowerCase().includes('completed') ||
|
||||
lastMessage.content.toLowerCase().includes('finished') ||
|
||||
lastMessage.content.toLowerCase().includes('done') ||
|
||||
lastMessage.content.toLowerCase().includes('workflow completed'))) {
|
||||
setWorkflowCompleted(true);
|
||||
}
|
||||
}
|
||||
}, [messages, workflowStatus]);
|
||||
|
||||
const handleSend = async () => {
|
||||
if (inputValue.trim()) {
|
||||
console.log('Sending message:', inputValue);
|
||||
|
||||
try {
|
||||
let result;
|
||||
|
||||
// If we have a completed workflow, send as follow-up using the existing workflow ID
|
||||
if (workflowCompleted && currentWorkflowId) {
|
||||
console.log('Sending follow-up message to workflow:', currentWorkflowId);
|
||||
result = await startWorkflow({
|
||||
prompt: inputValue,
|
||||
listFileId: []
|
||||
}, currentWorkflowId);
|
||||
|
||||
if (result.success) {
|
||||
console.log('Follow-up message sent successfully');
|
||||
// Reset workflow completion state to resume polling
|
||||
setWorkflowCompleted(false);
|
||||
}
|
||||
} else {
|
||||
// Start a new workflow
|
||||
console.log('Starting new workflow');
|
||||
// Reset previous workflow state when starting a new one
|
||||
setCurrentWorkflowId(null);
|
||||
setWorkflowCompleted(false);
|
||||
|
||||
result = await startWorkflow({
|
||||
prompt: inputValue,
|
||||
listFileId: []
|
||||
});
|
||||
|
||||
if (result.success && result.data) {
|
||||
console.log('Workflow started successfully:', result.data);
|
||||
// Set the workflow ID to start polling for messages
|
||||
setCurrentWorkflowId(result.data.id);
|
||||
setWorkflowCompleted(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (result.success) {
|
||||
// Clear the input after successful send
|
||||
setInputValue("");
|
||||
// Call onPromptUsed if a prompt was used
|
||||
if (selectedPrompt && onPromptUsed) {
|
||||
onPromptUsed();
|
||||
}
|
||||
} else {
|
||||
console.error('Failed to send message:', result.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error sending message:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyPress = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSend();
|
||||
}
|
||||
};
|
||||
|
||||
const startNewWorkflow = () => {
|
||||
setCurrentWorkflowId(null);
|
||||
setWorkflowCompleted(false);
|
||||
setInputValue("");
|
||||
if (onWorkflowIdChange) {
|
||||
onWorkflowIdChange(null);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (currentWorkflowId && onWorkflowIdChange) {
|
||||
onWorkflowIdChange(currentWorkflowId);
|
||||
}
|
||||
}, [currentWorkflowId, onWorkflowIdChange]);
|
||||
|
||||
// Handle workflow resumption
|
||||
useEffect(() => {
|
||||
if (resumeWorkflowId && resumeWorkflowId !== currentWorkflowId) {
|
||||
console.log('Resuming workflow:', resumeWorkflowId);
|
||||
setCurrentWorkflowId(resumeWorkflowId);
|
||||
setWorkflowCompleted(false);
|
||||
setInputValue("");
|
||||
}
|
||||
}, [resumeWorkflowId, currentWorkflowId]);
|
||||
|
||||
return (
|
||||
<div className={styles.chat_area}>
|
||||
<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 ? 'Sending follow-up message...' : 'Sending message...'}</p>
|
||||
</div>
|
||||
)}
|
||||
{startError && (
|
||||
<div className={styles.error_message}>
|
||||
<p>Error: {startError}</p>
|
||||
</div>
|
||||
)}
|
||||
{messagesError && (
|
||||
<div className={styles.error_message}>
|
||||
<p>Error loading messages: {messagesError}</p>
|
||||
</div>
|
||||
)}
|
||||
{currentWorkflowId && messagesLoading && messages.length === 0 && (
|
||||
<div className={styles.loading_message}>
|
||||
<p>Loading workflow messages...</p>
|
||||
</div>
|
||||
)}
|
||||
{messages.length > 0 ? (
|
||||
messages.map((message, index) => (
|
||||
<div
|
||||
key={message.id || index}
|
||||
className={`${styles.message} ${styles[`message_${message.role}`]}`}
|
||||
>
|
||||
<div className={styles.message_role}>
|
||||
{message.role === 'user' ? 'You' :
|
||||
message.role === 'assistant' ? 'Assistant' : 'System'}
|
||||
</div>
|
||||
<div className={styles.message_content}>
|
||||
{message.content}
|
||||
</div>
|
||||
{message.timestamp && (
|
||||
<div className={styles.message_timestamp}>
|
||||
{new Date(message.timestamp).toLocaleTimeString()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
) : !currentWorkflowId ? (
|
||||
<p className={styles.placeholder_text}>Start a conversation by typing a message...</p>
|
||||
) : null}
|
||||
{currentWorkflowId && !workflowCompleted && (
|
||||
<div className={styles.workflow_status}>
|
||||
<p>
|
||||
Workflow {currentWorkflowId.substring(0, 8)}... is {workflowStatus?.status || 'running'}
|
||||
{workflowStatus?.currentRound && ` (Round ${workflowStatus.currentRound})`}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{workflowCompleted && (
|
||||
<div className={styles.completion_message}>
|
||||
<p>Workflow completed! You can continue the conversation or start a new workflow.</p>
|
||||
<button
|
||||
className={styles.new_workflow_button}
|
||||
onClick={startNewWorkflow}
|
||||
>
|
||||
Start New Workflow
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
</motion.div>
|
||||
<motion.div
|
||||
className={styles.chat_input}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.3, duration: 0.3, ease: "easeOut" }}
|
||||
>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={inputValue}
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
onKeyPress={handleKeyPress}
|
||||
placeholder={workflowCompleted ? "Continue the conversation..." : "Type your message..."}
|
||||
className={styles.message_input}
|
||||
disabled={startingWorkflow}
|
||||
/>
|
||||
<motion.button
|
||||
className={styles.send_button}
|
||||
onClick={handleSend}
|
||||
disabled={startingWorkflow || !inputValue.trim()}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
transition={{ duration: 0.2, ease: "easeOut" }}
|
||||
>
|
||||
<LuSendHorizontal className={styles.send_button_icon}/>
|
||||
</motion.button>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardChatArea;
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
.chat_history {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 20px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.history_title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.workflowCount {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
background-color: #f5f5f5;
|
||||
padding: 4px 12px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.scrollableContent {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.workflowsList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.emptyState {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 200px;
|
||||
color: #999;
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.loadingContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.loadingText {
|
||||
color: #666;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.errorContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 200px;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.errorText {
|
||||
color: #f44336;
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.retryButton {
|
||||
background-color: #2196F3;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.retryButton:hover {
|
||||
background-color: #1976D2;
|
||||
}
|
||||
|
||||
/* Scrollbar styling */
|
||||
.scrollableContent::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.scrollableContent::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.scrollableContent::-webkit-scrollbar-thumb {
|
||||
background: #c1c1c1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.scrollableContent::-webkit-scrollbar-thumb:hover {
|
||||
background: #a8a8a8;
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
import React from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { useWorkflows } from "../../../../hooks/useWorkflows";
|
||||
import DashboardChatHistoryItem from "./DashboardChatHistoryItem";
|
||||
|
||||
import styles from './DashboardChatHistory.module.css';
|
||||
|
||||
interface DashboardChatHistoryProps {
|
||||
onWorkflowResume?: (workflowId: string) => void;
|
||||
}
|
||||
|
||||
const DashboardChatHistory: React.FC<DashboardChatHistoryProps> = ({ onWorkflowResume }) => {
|
||||
const { workflows, loading, error, refetch } = useWorkflows();
|
||||
|
||||
const handleWorkflowResume = (workflowId: string) => {
|
||||
if (onWorkflowResume) {
|
||||
onWorkflowResume(workflowId);
|
||||
}
|
||||
console.log('Resuming workflow:', workflowId);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<motion.div
|
||||
className={styles.chat_history}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.2, duration: 0.3, ease: "easeOut" }}
|
||||
>
|
||||
<div className={styles.loadingContainer}>
|
||||
<div className={styles.loadingText}>Loading workflows...</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<motion.div
|
||||
className={styles.chat_history}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.2, duration: 0.3, ease: "easeOut" }}
|
||||
>
|
||||
<div className={styles.errorContainer}>
|
||||
<div className={styles.errorText}>Error loading workflows: {error}</div>
|
||||
<button
|
||||
onClick={refetch}
|
||||
className={styles.retryButton}
|
||||
>
|
||||
Try Again
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
className={styles.chat_history}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.2, duration: 0.3, ease: "easeOut" }}
|
||||
>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.header}>
|
||||
<h2 className={styles.history_title}>Workflow History</h2>
|
||||
<div className={styles.workflowCount}>
|
||||
{workflows.length} {workflows.length === 1 ? 'Workflow' : 'Workflows'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.scrollableContent}>
|
||||
{workflows.length === 0 ? (
|
||||
<div className={styles.emptyState}>
|
||||
No workflows available
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.workflowsList}>
|
||||
{workflows.map((workflow) => (
|
||||
<DashboardChatHistoryItem
|
||||
key={workflow.id}
|
||||
workflow={workflow}
|
||||
onDelete={refetch}
|
||||
onResume={handleWorkflowResume}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardChatHistory;
|
||||
|
|
@ -0,0 +1,178 @@
|
|||
.workflowItem {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #e0e0e0;
|
||||
margin-bottom: 12px;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.workflowItem:hover {
|
||||
border-color: #d0d0d0;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.workflowMain {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
padding: 16px;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.workflowContent {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.workflowInfo {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.workflowId {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin: 0 0 8px 0;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.workflowMeta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.workflowStatus {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.workflowRound {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
background-color: #f5f5f5;
|
||||
padding: 2px 6px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.workflowDates {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.workflowDate {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin: 0;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.workflowDescription {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.messagePreview {
|
||||
margin-bottom: 8px;
|
||||
padding: 8px;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 6px;
|
||||
border-left: 3px solid #2196F3;
|
||||
}
|
||||
|
||||
.previewLabel {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #666;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.previewText {
|
||||
font-size: 13px;
|
||||
color: #444;
|
||||
margin: 0;
|
||||
line-height: 1.4;
|
||||
word-break: break-word;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.workflowName {
|
||||
font-size: 14px;
|
||||
color: #555;
|
||||
margin: 0;
|
||||
line-height: 1.4;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.actionButtons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.actionButton {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.actionButton:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.resumeButton {
|
||||
background-color: #4CAF50;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.resumeButton:hover:not(:disabled) {
|
||||
background-color: #45a049;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.deleteButton {
|
||||
background-color: #f44336;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.deleteButton:hover:not(:disabled) {
|
||||
background-color: #da190b;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.deletingMessage {
|
||||
padding: 8px 16px;
|
||||
background-color: #fff3cd;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
color: #856404;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.workflowMain {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.actionButtons {
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,155 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { FaArrowRight } from 'react-icons/fa';
|
||||
import { AiOutlineDelete } from 'react-icons/ai';
|
||||
import { useWorkflowOperations, useWorkflowMessages, Workflow } from '../../../../hooks/useWorkflows';
|
||||
import styles from './DashboardChatHistoryItem.module.css';
|
||||
|
||||
interface DashboardChatHistoryItemProps {
|
||||
workflow: Workflow;
|
||||
onDelete?: () => void;
|
||||
onResume: (workflowId: string) => void;
|
||||
}
|
||||
|
||||
function DashboardChatHistoryItem({ workflow, onDelete, onResume }: DashboardChatHistoryItemProps) {
|
||||
const { deleteWorkflow, deletingWorkflows } = useWorkflowOperations();
|
||||
const { messages } = useWorkflowMessages(workflow.id);
|
||||
|
||||
const isDeleting = deletingWorkflows.has(workflow.id);
|
||||
|
||||
// Get the first user message as preview
|
||||
const firstUserMessage = messages.find(msg => msg.role === 'user');
|
||||
const messagePreview = firstUserMessage?.content || 'No message content available';
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (window.confirm(`Are you sure you want to delete workflow "${workflow.id.substring(0, 8)}..."?`)) {
|
||||
const success = await deleteWorkflow(workflow.id);
|
||||
if (success && onDelete) {
|
||||
onDelete();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleResume = () => {
|
||||
onResume(workflow.id);
|
||||
};
|
||||
|
||||
const formatDate = (dateString?: string) => {
|
||||
if (!dateString) return 'Unknown date';
|
||||
try {
|
||||
return new Date(dateString).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
} catch (error) {
|
||||
return 'Invalid date';
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status.toLowerCase()) {
|
||||
case 'completed':
|
||||
case 'finished':
|
||||
case 'done':
|
||||
return '#4CAF50';
|
||||
case 'running':
|
||||
case 'processing':
|
||||
return '#2196F3';
|
||||
case 'error':
|
||||
case 'failed':
|
||||
return '#F44336';
|
||||
case 'stopped':
|
||||
case 'cancelled':
|
||||
return '#FF9800';
|
||||
default:
|
||||
return '#9E9E9E';
|
||||
}
|
||||
};
|
||||
|
||||
const truncateMessage = (message: string, maxLength: number = 150) => {
|
||||
if (message.length <= maxLength) return message;
|
||||
return message.substring(0, maxLength) + '...';
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.workflowItem}>
|
||||
<div className={styles.workflowMain}>
|
||||
<div className={styles.workflowContent}>
|
||||
<div className={styles.workflowInfo}>
|
||||
<h3 className={styles.workflowId}>
|
||||
Workflow {workflow.id.substring(0, 8)}...
|
||||
</h3>
|
||||
<div className={styles.workflowMeta}>
|
||||
<span
|
||||
className={styles.workflowStatus}
|
||||
style={{ color: getStatusColor(workflow.status) }}
|
||||
>
|
||||
{workflow.status.toUpperCase()}
|
||||
</span>
|
||||
{workflow.currentRound && (
|
||||
<span className={styles.workflowRound}>
|
||||
Round {workflow.currentRound}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.workflowDates}>
|
||||
{workflow.startedAt && (
|
||||
<p className={styles.workflowDate}>
|
||||
Started: {formatDate(workflow.startedAt)}
|
||||
</p>
|
||||
)}
|
||||
{workflow.lastActivity && (
|
||||
<p className={styles.workflowDate}>
|
||||
Last Activity: {formatDate(workflow.lastActivity)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.workflowDescription}>
|
||||
<div className={styles.messagePreview}>
|
||||
<span className={styles.previewLabel}>First message:</span>
|
||||
<p className={styles.previewText}>
|
||||
{truncateMessage(messagePreview)}
|
||||
</p>
|
||||
</div>
|
||||
{workflow.name && (
|
||||
<p className={styles.workflowName}>
|
||||
{workflow.name}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.actionButtons}>
|
||||
<button
|
||||
onClick={handleResume}
|
||||
className={`${styles.actionButton} ${styles.resumeButton}`}
|
||||
title="Resume workflow"
|
||||
>
|
||||
<FaArrowRight size={16} />
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleDelete}
|
||||
disabled={isDeleting}
|
||||
className={`${styles.actionButton} ${styles.deleteButton}`}
|
||||
title="Delete workflow"
|
||||
>
|
||||
<AiOutlineDelete size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isDeleting && (
|
||||
<div className={styles.deletingMessage}>
|
||||
Deleting workflow...
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DashboardChatHistoryItem;
|
||||
307
src/components/Dashboard/DashboardLog/DashboardLog.module.css
Normal file
307
src/components/Dashboard/DashboardLog/DashboardLog.module.css
Normal file
|
|
@ -0,0 +1,307 @@
|
|||
.dashboard_log {
|
||||
display: flex;
|
||||
padding: 20px;
|
||||
flex-direction: column;
|
||||
align-self: stretch;
|
||||
border-radius: 30px;
|
||||
border: 1px solid var(--f-1-f-1-f-1, #F1F1F1);
|
||||
background: var(--Grayscale-True-White, #FFF);
|
||||
position: relative;
|
||||
box-shadow: 0px 2px 6px 0px rgba(194, 194, 194, 0.10);
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dashboard_log.expanded {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.log_header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.log_title_div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.log_title {
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: normal;
|
||||
border: none;
|
||||
background: none;
|
||||
outline: none;
|
||||
color: var(--Grayscale-Black, #24262B);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.log_title_collapsed {
|
||||
opacity: 50%;
|
||||
color: #A0A0A0;
|
||||
}
|
||||
|
||||
.collapseIcon {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.collapseIcon:hover {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.horizontalLine {
|
||||
width: 100%;
|
||||
background-color: black;
|
||||
height: 2px;
|
||||
margin-top: 19px;
|
||||
}
|
||||
|
||||
.horizontalLineLight {
|
||||
width: calc(100%);
|
||||
background-color: #F1F1F1;
|
||||
height: 2px;
|
||||
margin-top: 39px;
|
||||
margin-left: -20px;
|
||||
position: absolute;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.log_content {
|
||||
margin-top: 20px;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.log_entries {
|
||||
background-color: #f9f9f9;
|
||||
border-radius: 15px;
|
||||
padding: 15px;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.log_entry {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.log_entry:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.log_timestamp {
|
||||
color: #666;
|
||||
min-width: 140px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.log_level_info {
|
||||
background-color: #4CAF50;
|
||||
color: white;
|
||||
padding: 2px 8px;
|
||||
border-radius: 30px;
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
min-width: 45px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.log_level_warning {
|
||||
background-color: #FF9800;
|
||||
color: white;
|
||||
padding: 2px 8px;
|
||||
border-radius: 30px;
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
min-width: 45px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.log_level_error {
|
||||
background-color: #F44336;
|
||||
color: white;
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
min-width: 45px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.log_message {
|
||||
flex: 1;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* Hacker-style console styles */
|
||||
.console_container {
|
||||
background-color: #0a0a0a;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
flex: 1;
|
||||
min-height: 200px;
|
||||
overflow-y: auto;
|
||||
font-family: 'Courier New', 'Monaco', 'Menlo', monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
color: #00ff00;
|
||||
border: 1px solid #333;
|
||||
box-shadow: inset 0 0 10px rgba(0, 255, 0, 0.1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.console_container::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.console_container::-webkit-scrollbar-track {
|
||||
background: #1a1a1a;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.console_container::-webkit-scrollbar-thumb {
|
||||
background: #333;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.console_container::-webkit-scrollbar-thumb:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
.console_content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.console_line {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 2px;
|
||||
animation: fadeIn 0.3s ease-in;
|
||||
}
|
||||
|
||||
.console_timestamp {
|
||||
color: #888;
|
||||
margin-right: 8px;
|
||||
font-weight: bold;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.console_level {
|
||||
margin-right: 8px;
|
||||
font-weight: bold;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.console_message {
|
||||
color: #00ff00;
|
||||
flex: 1;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.console_data {
|
||||
width: 100%;
|
||||
margin-top: 4px;
|
||||
margin-left: 20px;
|
||||
color: #00aaff;
|
||||
font-size: 11px;
|
||||
white-space: pre-wrap;
|
||||
background-color: rgba(0, 170, 255, 0.1);
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
border-left: 3px solid #00aaff;
|
||||
}
|
||||
|
||||
.console_prompt {
|
||||
color: #00ff00;
|
||||
margin-right: 8px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.console_text {
|
||||
color: #00ff00;
|
||||
}
|
||||
|
||||
.console_placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
opacity: 0.7;
|
||||
font-style: italic;
|
||||
padding: 10px 0;
|
||||
min-height: 30px;
|
||||
}
|
||||
|
||||
.console_loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px 0;
|
||||
min-height: 30px;
|
||||
}
|
||||
|
||||
.console_cursor {
|
||||
color: #00ff00;
|
||||
animation: blink 1s infinite;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.console_error {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px 0;
|
||||
min-height: 30px;
|
||||
}
|
||||
|
||||
.console_error .console_text {
|
||||
color: #ff4444;
|
||||
}
|
||||
|
||||
.console_empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
opacity: 0.7;
|
||||
font-style: italic;
|
||||
padding: 10px 0;
|
||||
min-height: 30px;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(5px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0%, 50% {
|
||||
opacity: 1;
|
||||
}
|
||||
51%, 100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
160
src/components/Dashboard/DashboardLog/DashboardLog.tsx
Normal file
160
src/components/Dashboard/DashboardLog/DashboardLog.tsx
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
import React, { useEffect, useRef } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { useWorkflowLogs, useWorkflowStatus } from "../../../hooks/useWorkflows";
|
||||
|
||||
import styles from './DashboardLog.module.css';
|
||||
|
||||
interface DashboardLogProps {
|
||||
isExpanded: boolean;
|
||||
workflowId: string | null;
|
||||
}
|
||||
|
||||
const DashboardLog: React.FC<DashboardLogProps> = ({ isExpanded, workflowId }) => {
|
||||
const { status: workflowStatus } = useWorkflowStatus(workflowId);
|
||||
|
||||
// Determine if workflow is completed
|
||||
const workflowCompleted = workflowStatus && (
|
||||
workflowStatus.status === 'completed' ||
|
||||
workflowStatus.status === 'finished' ||
|
||||
workflowStatus.status === 'done' ||
|
||||
workflowStatus.status === 'stopped'
|
||||
);
|
||||
|
||||
const { logs, loading, error } = useWorkflowLogs(workflowId, undefined, !!workflowId, !!workflowCompleted);
|
||||
const logsEndRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Auto-scroll to bottom when new logs arrive
|
||||
useEffect(() => {
|
||||
if (logsEndRef.current) {
|
||||
logsEndRef.current.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
}, [logs]);
|
||||
|
||||
const formatTimestamp = (timestamp: string) => {
|
||||
if (!timestamp) return '00:00:00';
|
||||
|
||||
try {
|
||||
return new Date(timestamp).toLocaleTimeString('en-US', {
|
||||
hour12: false,
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
});
|
||||
} catch (error) {
|
||||
return '00:00:00';
|
||||
}
|
||||
};
|
||||
|
||||
const getLevelColor = (level: string) => {
|
||||
if (!level) return '#00ff00'; // Default green if level is undefined
|
||||
|
||||
switch (level.toLowerCase()) {
|
||||
case 'error':
|
||||
return '#ff4444';
|
||||
case 'warn':
|
||||
case 'warning':
|
||||
return '#ffaa00';
|
||||
case 'info':
|
||||
return '#00aaff';
|
||||
case 'debug':
|
||||
return '#888888';
|
||||
default:
|
||||
return '#00ff00';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
className={`${styles.dashboard_log} ${isExpanded ? styles.expanded : ''}`}
|
||||
layout
|
||||
transition={{ duration: 0.4, ease: "easeOut" }}
|
||||
>
|
||||
<motion.div
|
||||
className={styles.log_header}
|
||||
layout
|
||||
transition={{ duration: 0.3, ease: "easeOut" }}
|
||||
>
|
||||
<div className={styles.log_title_div}>
|
||||
<motion.div
|
||||
className={styles.log_title}
|
||||
whileHover={{ scale: 1.05 }}
|
||||
transition={{ duration: 0.2, ease: "easeOut" }}
|
||||
>
|
||||
Log {workflowId && `- Workflow ${workflowId.substring(0, 8)}...`}
|
||||
</motion.div>
|
||||
<motion.div
|
||||
className={styles.horizontalLine}
|
||||
initial={{ opacity: 0, width: "0%" }}
|
||||
animate={{ opacity: 1, width: "100%" }}
|
||||
transition={{ duration: 0.3, ease: "easeOut" }}
|
||||
></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.log_content}
|
||||
initial={{ opacity: 0, height: 0 }}
|
||||
animate={{ opacity: 1, height: "auto" }}
|
||||
transition={{ duration: 0.4, ease: "easeOut" }}
|
||||
>
|
||||
<div className={styles.console_container}>
|
||||
<div className={styles.console_content}>
|
||||
{!workflowId ? (
|
||||
<div className={styles.console_placeholder}>
|
||||
<span className={styles.console_prompt}>$</span>
|
||||
<span className={styles.console_text}>Waiting for workflow to start...</span>
|
||||
</div>
|
||||
) : loading && logs.length === 0 ? (
|
||||
<div className={styles.console_loading}>
|
||||
<span className={styles.console_prompt}>$</span>
|
||||
<span className={styles.console_text}>Loading workflow logs...</span>
|
||||
<span className={styles.console_cursor}>_</span>
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className={styles.console_error}>
|
||||
<span className={styles.console_prompt}>$</span>
|
||||
<span className={styles.console_text}>Error loading logs: {error}</span>
|
||||
</div>
|
||||
) : logs.length > 0 ? (
|
||||
logs.map((log, index) => (
|
||||
<div key={log.id || index} className={styles.console_line}>
|
||||
<span className={styles.console_timestamp}>
|
||||
[{formatTimestamp(log.timestamp || '')}]
|
||||
</span>
|
||||
<span
|
||||
className={styles.console_level}
|
||||
style={{ color: getLevelColor(log.level || 'info') }}
|
||||
>
|
||||
[{(log.level || 'INFO').toUpperCase()}]
|
||||
</span>
|
||||
<span className={styles.console_message}>
|
||||
{log.message || 'No message'}
|
||||
</span>
|
||||
{log.data && (
|
||||
<div className={styles.console_data}>
|
||||
{JSON.stringify(log.data, null, 2)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className={styles.console_empty}>
|
||||
<span className={styles.console_prompt}>$</span>
|
||||
<span className={styles.console_text}>No logs available for this workflow</span>
|
||||
</div>
|
||||
)}
|
||||
<div ref={logsEndRef} />
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardLog;
|
||||
|
|
@ -0,0 +1,153 @@
|
|||
.dashboard_prompt {
|
||||
display: flex;
|
||||
padding: 20px;
|
||||
flex-direction: column;
|
||||
align-self: stretch;
|
||||
border-radius: 30px;
|
||||
border: 1px solid var(--f-1-f-1-f-1, #F1F1F1);
|
||||
background: var(--Grayscale-True-White, #FFF);
|
||||
position: relative;
|
||||
box-shadow: 0px 2px 6px 0px rgba(194, 194, 194, 0.10);
|
||||
width: 100%;
|
||||
transition: height 0.3s ease;
|
||||
}
|
||||
|
||||
.dashboard_prompt:not(.collapsed) {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.dashboard_prompt.collapsed {
|
||||
height: auto;
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.prompt_header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.prompt_button_div {
|
||||
display: flex;
|
||||
align-self: stretch;
|
||||
gap: 30px;
|
||||
}
|
||||
|
||||
.prompt_button {
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: normal;
|
||||
border: none;
|
||||
background: none;
|
||||
outline: none;
|
||||
color: var(--Grayscale-Black, #24262B);
|
||||
transition: opacity 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
|
||||
.prompt_button_inactive {
|
||||
opacity: 50%;
|
||||
}
|
||||
|
||||
.prompt_button_collapsed {
|
||||
opacity: 50%;
|
||||
color: #A0A0A0;
|
||||
}
|
||||
|
||||
.buttonWrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.expandIcon {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #666;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.expandIcon:hover {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.horizontalLine {
|
||||
width: 100%;
|
||||
background-color: black;
|
||||
height: 2px;
|
||||
margin-top: 19px;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.horizontalLineLight {
|
||||
width: calc(100%);
|
||||
background-color: #F1F1F1;
|
||||
height: 2px;
|
||||
margin-top: 39px;
|
||||
margin-left: -20px;
|
||||
position: absolute;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.content_wrapper {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.content_area {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: all 0.3s ease;
|
||||
opacity: 1;
|
||||
position: relative;
|
||||
}
|
||||
.content_collapsed {
|
||||
opacity: 0;
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.scrollableContent {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: 0 0.5rem 2rem 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.collapseUp {
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
.collapseContent {
|
||||
will-change: transform, opacity;
|
||||
transition: transform 0.3s cubic-bezier(0.4,0,0.2,1), opacity 0.3s cubic-bezier(0.4,0,0.2,1);
|
||||
}
|
||||
.collapseContent.collapsed {
|
||||
transform: translateY(-100%);
|
||||
opacity: 0;
|
||||
}
|
||||
.collapseContent.expanded {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
98
src/components/Dashboard/DashboardPrompt/DashboardPrompt.tsx
Normal file
98
src/components/Dashboard/DashboardPrompt/DashboardPrompt.tsx
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { MdExpandMore, MdExpandLess } from "react-icons/md";
|
||||
|
||||
import DashboardPromptSettings from './DashboardPromptSettings/DashboardPromptSettings';
|
||||
import DashboardPromptSet from './DashboardPromptSet/DashboardPromptSet';
|
||||
import { Prompt } from '../../../hooks/usePrompts';
|
||||
|
||||
import styles from './DashboardPrompt.module.css';
|
||||
|
||||
interface DashboardPromptProps {
|
||||
onPromptRun: (prompt: Prompt) => void;
|
||||
isCollapsed: boolean;
|
||||
onToggleCollapse: () => void;
|
||||
}
|
||||
|
||||
const DashboardPrompt: React.FC<DashboardPromptProps> = ({
|
||||
onPromptRun,
|
||||
isCollapsed,
|
||||
onToggleCollapse
|
||||
}) => {
|
||||
const [activeTab, setActiveTab] = useState("Prompt Set");
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
useEffect(() => {
|
||||
const expandedPrompt = searchParams.get('expandedPrompt');
|
||||
const promptId = searchParams.get('promptId');
|
||||
|
||||
if (expandedPrompt) {
|
||||
setActiveTab("Prompt Set");
|
||||
} else if (promptId) {
|
||||
setActiveTab("Einstellungen");
|
||||
}
|
||||
}, [searchParams]);
|
||||
|
||||
return (
|
||||
<div className={`${styles.dashboard_prompt} ${isCollapsed ? styles.collapsed : ''}`}>
|
||||
<div className={ styles.prompt_header }>
|
||||
<div className={ styles.prompt_button_div }>
|
||||
{[
|
||||
"Prompt Set",
|
||||
"Einstellungen"
|
||||
].map((tab) => (
|
||||
<div key={tab} className={styles.buttonWrapper}>
|
||||
<button
|
||||
className={`${styles.prompt_button} ${
|
||||
!isCollapsed
|
||||
? (activeTab === tab ? styles.prompt_button_active : styles.prompt_button_inactive)
|
||||
: styles.prompt_button_collapsed
|
||||
}`}
|
||||
onClick={()=> setActiveTab(tab)}
|
||||
>
|
||||
{ tab }
|
||||
</button>
|
||||
{!isCollapsed && activeTab === tab && (
|
||||
<div className={styles.horizontalLine}></div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div
|
||||
className={styles.expandIcon}
|
||||
onClick={onToggleCollapse}
|
||||
style={{
|
||||
transform: !isCollapsed ? 'rotate(0deg)' : 'rotate(180deg)',
|
||||
transition: 'transform 0.3s ease'
|
||||
}}
|
||||
>
|
||||
<MdExpandLess size={24} />
|
||||
</div>
|
||||
</div>
|
||||
{!isCollapsed && (
|
||||
<div className={styles.horizontalLineLight}></div>
|
||||
)}
|
||||
{!isCollapsed && (
|
||||
<div
|
||||
className={styles.content_wrapper}
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
flex: 1,
|
||||
minHeight: 0
|
||||
}}
|
||||
>
|
||||
<div style={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column' }}>
|
||||
{activeTab === "Prompt Set" ? (
|
||||
<DashboardPromptSet onPromptRun={onPromptRun} />
|
||||
) : (
|
||||
<DashboardPromptSettings />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DashboardPrompt;
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: left;
|
||||
align-items: center;
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
flex-shrink: 0;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.addButton {
|
||||
border-radius: 30px;
|
||||
background: var(--Brand-Green-Green, #3A8088);
|
||||
color: white;
|
||||
border: none;
|
||||
outline: none;
|
||||
text-align: left;
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.addButton:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.promptCount {
|
||||
font-size: 0.875rem;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.scrollableContent {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding-right: 0.5rem;
|
||||
padding-bottom: 2rem;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.scrollableContent::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.scrollableContent::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.scrollableContent::-webkit-scrollbar-thumb {
|
||||
background: #ccc;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.scrollableContent::-webkit-scrollbar-thumb:hover {
|
||||
background: #999;
|
||||
}
|
||||
|
||||
.promptsList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.loadingContainer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.loadingText {
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.errorContainer {
|
||||
padding: 1rem;
|
||||
background-color: #fef2f2;
|
||||
border: 1px solid #fecaca;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.errorText {
|
||||
color: #b91c1c;
|
||||
}
|
||||
|
||||
.retryButton {
|
||||
margin-top: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
background-color: #dc2626;
|
||||
color: white;
|
||||
border-radius: 0.375rem;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.retryButton:hover {
|
||||
background-color: #b91c1c;
|
||||
}
|
||||
|
||||
.emptyState {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
import React from 'react';
|
||||
import { usePrompts, Prompt } from '../../../../hooks/usePrompts';
|
||||
import DashboardPromptSetItem from './DashboardPromptSetItem';
|
||||
import styles from './DashboardPromptSet.module.css';
|
||||
import { FaPlus } from 'react-icons/fa';
|
||||
|
||||
interface DashboardPromptSetProps {
|
||||
onPromptRun: (prompt: Prompt) => void;
|
||||
}
|
||||
|
||||
function DashboardPromptSet({ onPromptRun }: DashboardPromptSetProps) {
|
||||
const { prompts, loading, error, refetch } = usePrompts();
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className={styles.loadingContainer}>
|
||||
<div className={styles.loadingText}>Prompts werden geladen...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className={styles.errorContainer}>
|
||||
<div className={styles.errorText}>Fehler beim Laden der Prompts: {error}</div>
|
||||
<button
|
||||
onClick={refetch}
|
||||
className={styles.retryButton}
|
||||
>
|
||||
Erneut versuchen
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.header}>
|
||||
<div className={styles.headerButtons}>
|
||||
<button className={styles.addButton} onClick={() => console.log('add prompt')}>
|
||||
<FaPlus />
|
||||
Neuer Prompt
|
||||
</button>
|
||||
</div>
|
||||
<div className={styles.promptCount}>
|
||||
{prompts.length} {prompts.length === 1 ? 'Prompt' : 'Prompts'}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div className={styles.scrollableContent}>
|
||||
{prompts.length === 0 ? (
|
||||
<div className={styles.emptyState}>
|
||||
Keine Prompts verfügbar
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.promptsList}>
|
||||
{prompts.map((prompt) => (
|
||||
<DashboardPromptSetItem
|
||||
key={prompt.id}
|
||||
prompt={prompt}
|
||||
onDelete={refetch}
|
||||
onRun={onPromptRun}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DashboardPromptSet;
|
||||
|
|
@ -0,0 +1,134 @@
|
|||
.promptItem {
|
||||
background: var(--Grayscale-Light-Gray, #F9F9F9);
|
||||
border-radius: 30px;
|
||||
display: flex;
|
||||
padding: 20px;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
align-self: stretch;
|
||||
justify-content: top;
|
||||
font-family: 'Avenir', sans-serif;
|
||||
gap: 11px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
|
||||
.promptMain {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
align-self: stretch;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.promptInfo {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.promptName {
|
||||
font-weight: 400;
|
||||
color: #000;
|
||||
margin:0;
|
||||
}
|
||||
|
||||
.promptDate {
|
||||
font: 14px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.promptText {
|
||||
overflow: hidden;
|
||||
height: auto;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
opacity: 0.5;
|
||||
margin:0;
|
||||
}
|
||||
|
||||
.promptText.p {
|
||||
margin:0;
|
||||
}
|
||||
|
||||
.actionButtons {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-self: flex-start;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.actionButton {
|
||||
padding: 0.5rem;
|
||||
border-radius: 12px;
|
||||
background: var(--Brand-Green-Green, #3A8088);
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.actionButton:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.runButton {
|
||||
border-radius: 12px;
|
||||
background: var(--Brand-Green-Green, #3A8088);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.runButton:hover:not(:disabled) {
|
||||
border-radius: 12px;
|
||||
background: var(--Brand-Green-Green, #3A8088);
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.shareButton {
|
||||
border-radius: 12px;
|
||||
background: var(--Brand-Green-Green, #3A8088);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.shareButton:hover:not(:disabled) {
|
||||
border-radius: 12px;
|
||||
background: var(--Brand-Green-Green, #3A8088);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.deleteButton {
|
||||
border-radius: 12px;
|
||||
background: var(--Brand-Green-Green, #3A8088);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.deleteButton:hover:not(:disabled) {
|
||||
border-radius: 12px;
|
||||
background: var(--Brand-Green-Green, #3A8088);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.promptContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
gap: 11px;
|
||||
max-width: calc(100% - 120px);
|
||||
}
|
||||
|
||||
.errorMessage {
|
||||
margin-top: 0.75rem;
|
||||
padding: 0.5rem;
|
||||
background-color: #fef2f2;
|
||||
border: 1px solid #fecaca;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.875rem;
|
||||
color: #b91c1c;
|
||||
}
|
||||
|
||||
.deletingMessage {
|
||||
margin-top: 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { FaArrowRight } from 'react-icons/fa';
|
||||
import { AiOutlineDelete } from 'react-icons/ai';
|
||||
import { BsShareFill } from 'react-icons/bs';
|
||||
import { usePromptOperations, Prompt } from '../../../../hooks/usePrompts';
|
||||
import styles from './DashboardPromptSetItem.module.css';
|
||||
|
||||
interface DashboardPromptSetItemProps {
|
||||
prompt: Prompt;
|
||||
onDelete?: () => void;
|
||||
onRun: (prompt: Prompt) => void;
|
||||
}
|
||||
|
||||
function DashboardPromptSetItem({ prompt, onDelete, onRun }: DashboardPromptSetItemProps) {
|
||||
const { handlePromptDelete, deletingPrompts, deleteError } = usePromptOperations();
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const isDeleting = deletingPrompts.has(prompt.id);
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (window.confirm(`Möchten Sie den Prompt "${prompt.name}" wirklich löschen?`)) {
|
||||
const success = await handlePromptDelete(prompt.id);
|
||||
if (success && onDelete) {
|
||||
onDelete();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleRun = () => {
|
||||
onRun(prompt);
|
||||
};
|
||||
|
||||
const handleShare = () => {
|
||||
console.log('Sharing prompt:', prompt);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.promptItem}
|
||||
>
|
||||
<div className={styles.promptMain}>
|
||||
<div className={styles.promptContent}>
|
||||
<div className={styles.promptInfo}>
|
||||
<h3 className={styles.promptName}>
|
||||
{prompt.name}
|
||||
</h3>
|
||||
{prompt.createdAt && (
|
||||
<p className={styles.promptDate}>
|
||||
Erstellt: {new Date(prompt.createdAt).toLocaleDateString('de-DE')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div ref={contentRef}>
|
||||
<p className={styles.promptText}>
|
||||
{prompt.content}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.actionButtons}>
|
||||
<button
|
||||
onClick={handleRun}
|
||||
className={`${styles.actionButton} ${styles.runButton}`}
|
||||
title="Prompt ausführen"
|
||||
>
|
||||
<FaArrowRight size={16} />
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleShare}
|
||||
className={`${styles.actionButton} ${styles.shareButton}`}
|
||||
title="Prompt teilen"
|
||||
>
|
||||
<BsShareFill size={16} />
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleDelete}
|
||||
disabled={isDeleting}
|
||||
className={`${styles.actionButton} ${styles.deleteButton}`}
|
||||
title="Prompt löschen"
|
||||
>
|
||||
<AiOutlineDelete size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{deleteError && (
|
||||
<div className={styles.errorMessage}>
|
||||
Fehler beim Löschen: {deleteError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isDeleting && (
|
||||
<div className={styles.deletingMessage}>
|
||||
Prompt wird gelöscht...
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DashboardPromptSetItem;
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
.promptArea {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.cancelContainer {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.cancelButton {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 8px 16px;
|
||||
background: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 20px;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
.cancelButton:hover {
|
||||
background-color: #f5f5f5;
|
||||
border-color: #ccc;
|
||||
}
|
||||
|
||||
.cancelIcon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
import React from 'react';
|
||||
import styles from './DashboardPromptSettings.module.css';
|
||||
|
||||
function DashboardPromptSettings() {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.content}>
|
||||
<h1 className={styles.title}>Dashboard Prompt Settings</h1>
|
||||
<p>Settings content will be added here in future updates.</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DashboardPromptSettings;
|
||||
|
|
@ -19,48 +19,42 @@ const useSidebarData = () => {
|
|||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Prompts',
|
||||
icon: BsChatDots,
|
||||
submenu: [],
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'Aktivitätszentrum',
|
||||
link: '/dashboard',
|
||||
icon: LuTicket,
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
id: '3',
|
||||
name: 'Dateien',
|
||||
link: '/dateien',
|
||||
icon: FaRegFileAlt,
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
id: '4',
|
||||
name: 'Mitglieder',
|
||||
link: '/mitglieder',
|
||||
icon: RiTeamLine,
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
id: '5',
|
||||
name: 'Nachrichten',
|
||||
link: '',
|
||||
icon: BiInfoSquare,
|
||||
},
|
||||
{
|
||||
id: '7',
|
||||
id: '6',
|
||||
name: 'Logs',
|
||||
link: '',
|
||||
icon: TbLogs ,
|
||||
},
|
||||
{
|
||||
id: '8',
|
||||
id: '7',
|
||||
name: 'Settings',
|
||||
link: '',
|
||||
icon: GoGear,
|
||||
},
|
||||
{
|
||||
id: '9',
|
||||
id: '8',
|
||||
name: 'Help',
|
||||
link: '',
|
||||
icon: BiInfoSquare,
|
||||
|
|
|
|||
128
src/hooks/usePrompts.ts
Normal file
128
src/hooks/usePrompts.ts
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import { useApiRequest } from './useApi';
|
||||
|
||||
// Prompt interfaces
|
||||
export interface Prompt {
|
||||
id: number;
|
||||
name: string;
|
||||
content: string;
|
||||
createdAt?: string;
|
||||
}
|
||||
|
||||
// Prompts list hook
|
||||
export function usePrompts() {
|
||||
const [prompts, setPrompts] = useState<Prompt[]>([]);
|
||||
const { request, isLoading: loading, error } = useApiRequest<null, Prompt[]>();
|
||||
|
||||
const fetchPrompts = async () => {
|
||||
try {
|
||||
const data = await request({
|
||||
url: '/api/prompts',
|
||||
method: 'get'
|
||||
});
|
||||
|
||||
setPrompts(data);
|
||||
} catch (error) {
|
||||
// Error is already handled by useApiRequest
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchPrompts();
|
||||
}, []);
|
||||
|
||||
return { prompts, loading, error, refetch: fetchPrompts };
|
||||
}
|
||||
|
||||
// Prompt operations hook
|
||||
export function usePromptOperations() {
|
||||
const [deletingPrompts, setDeletingPrompts] = useState<Set<number>>(new Set());
|
||||
const [creatingPrompt, setCreatingPrompt] = useState(false);
|
||||
const [updatingPrompts, setUpdatingPrompts] = useState<Set<number>>(new Set());
|
||||
const { request, error: apiError, isLoading } = useApiRequest();
|
||||
const [deleteError, setDeleteError] = useState<string | null>(null);
|
||||
const [createError, setCreateError] = useState<string | null>(null);
|
||||
const [updateError, setUpdateError] = useState<string | null>(null);
|
||||
|
||||
const handlePromptDelete = async (promptId: number) => {
|
||||
setDeleteError(null);
|
||||
setDeletingPrompts(prev => new Set(prev).add(promptId));
|
||||
|
||||
try {
|
||||
await request({
|
||||
url: `/api/prompts/${promptId}`,
|
||||
method: 'delete'
|
||||
});
|
||||
|
||||
// Add a small delay to ensure backend has time to process
|
||||
await new Promise(resolve => setTimeout(resolve, 300));
|
||||
return true;
|
||||
} catch (error: any) {
|
||||
setDeleteError(error.message);
|
||||
return false;
|
||||
} finally {
|
||||
setDeletingPrompts(prev => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.delete(promptId);
|
||||
return newSet;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handlePromptCreate = async (promptData: { name: string; content: string }) => {
|
||||
setCreateError(null);
|
||||
setCreatingPrompt(true);
|
||||
|
||||
try {
|
||||
const newPrompt = await request({
|
||||
url: '/api/prompts',
|
||||
method: 'post',
|
||||
data: promptData
|
||||
});
|
||||
|
||||
return { success: true, promptData: newPrompt };
|
||||
} catch (error: any) {
|
||||
setCreateError(error.message);
|
||||
return { success: false, error: error.message };
|
||||
} finally {
|
||||
setCreatingPrompt(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePromptUpdate = async (promptId: number, promptData: { name?: string; content?: string }) => {
|
||||
setUpdateError(null);
|
||||
setUpdatingPrompts(prev => new Set(prev).add(promptId));
|
||||
|
||||
try {
|
||||
const updatedPrompt = await request({
|
||||
url: `/api/prompts/${promptId}`,
|
||||
method: 'put',
|
||||
data: promptData
|
||||
});
|
||||
|
||||
return { success: true, promptData: updatedPrompt };
|
||||
} catch (error: any) {
|
||||
setUpdateError(error.message);
|
||||
return { success: false, error: error.message };
|
||||
} finally {
|
||||
setUpdatingPrompts(prev => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.delete(promptId);
|
||||
return newSet;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
deletingPrompts,
|
||||
creatingPrompt,
|
||||
updatingPrompts,
|
||||
deleteError,
|
||||
createError,
|
||||
updateError,
|
||||
handlePromptDelete,
|
||||
handlePromptCreate,
|
||||
handlePromptUpdate,
|
||||
isLoading
|
||||
};
|
||||
}
|
||||
260
src/hooks/useWorkflows.ts
Normal file
260
src/hooks/useWorkflows.ts
Normal file
|
|
@ -0,0 +1,260 @@
|
|||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useApiRequest } from './useApi';
|
||||
|
||||
// Workflow interfaces
|
||||
export interface Workflow {
|
||||
id: string;
|
||||
name?: string;
|
||||
status: string;
|
||||
startedAt?: string;
|
||||
lastActivity?: string;
|
||||
currentRound?: number;
|
||||
dataStats?: Record<string, any>;
|
||||
userId?: number;
|
||||
messageIds?: string[];
|
||||
}
|
||||
|
||||
export interface WorkflowMessage {
|
||||
id: string;
|
||||
content: string;
|
||||
role: 'user' | 'assistant' | 'system';
|
||||
timestamp?: string;
|
||||
sequenceNo?: number;
|
||||
fileIds?: number[];
|
||||
}
|
||||
|
||||
export interface WorkflowLog {
|
||||
id: string;
|
||||
level: string;
|
||||
message: string;
|
||||
timestamp: string;
|
||||
data?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface StartWorkflowRequest {
|
||||
prompt: string;
|
||||
listFileId: number[];
|
||||
}
|
||||
|
||||
export interface StartWorkflowResponse {
|
||||
id: string;
|
||||
status: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
// Workflows list hook
|
||||
export function useWorkflows() {
|
||||
const [workflows, setWorkflows] = useState<Workflow[]>([]);
|
||||
const { request, isLoading: loading, error } = useApiRequest<null, Workflow[]>();
|
||||
|
||||
const fetchWorkflows = async () => {
|
||||
try {
|
||||
const data = await request({
|
||||
url: '/api/workflows',
|
||||
method: 'get'
|
||||
});
|
||||
|
||||
setWorkflows(data);
|
||||
} catch (error) {
|
||||
// Error is already handled by useApiRequest
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchWorkflows();
|
||||
}, []);
|
||||
|
||||
return { workflows, loading, error, refetch: fetchWorkflows };
|
||||
}
|
||||
|
||||
// Workflow operations hook
|
||||
export function useWorkflowOperations() {
|
||||
const [startingWorkflow, setStartingWorkflow] = useState(false);
|
||||
const [stoppingWorkflows, setStoppingWorkflows] = useState<Set<string>>(new Set());
|
||||
const [deletingWorkflows, setDeletingWorkflows] = useState<Set<string>>(new Set());
|
||||
const { request, error: apiError, isLoading } = useApiRequest();
|
||||
const [startError, setStartError] = useState<string | null>(null);
|
||||
const [stopError, setStopError] = useState<string | null>(null);
|
||||
const [deleteError, setDeleteError] = useState<string | null>(null);
|
||||
|
||||
const startWorkflow = async (workflowData: StartWorkflowRequest, workflowId?: string) => {
|
||||
setStartError(null);
|
||||
setStartingWorkflow(true);
|
||||
|
||||
try {
|
||||
const response = await request({
|
||||
url: '/api/workflows/start',
|
||||
method: 'post',
|
||||
data: workflowData,
|
||||
params: workflowId ? { workflowId } : undefined
|
||||
}) as StartWorkflowResponse;
|
||||
|
||||
return { success: true, data: response };
|
||||
} catch (error: any) {
|
||||
setStartError(error.message);
|
||||
return { success: false, error: error.message };
|
||||
} finally {
|
||||
setStartingWorkflow(false);
|
||||
}
|
||||
};
|
||||
|
||||
const stopWorkflow = async (workflowId: string) => {
|
||||
setStopError(null);
|
||||
setStoppingWorkflows(prev => new Set(prev).add(workflowId));
|
||||
|
||||
try {
|
||||
await request({
|
||||
url: `/api/workflows/${workflowId}/stop`,
|
||||
method: 'post'
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch (error: any) {
|
||||
setStopError(error.message);
|
||||
return false;
|
||||
} finally {
|
||||
setStoppingWorkflows(prev => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.delete(workflowId);
|
||||
return newSet;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const deleteWorkflow = async (workflowId: string) => {
|
||||
setDeleteError(null);
|
||||
setDeletingWorkflows(prev => new Set(prev).add(workflowId));
|
||||
|
||||
try {
|
||||
await request({
|
||||
url: `/api/workflows/${workflowId}`,
|
||||
method: 'delete'
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch (error: any) {
|
||||
setDeleteError(error.message);
|
||||
return false;
|
||||
} finally {
|
||||
setDeletingWorkflows(prev => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.delete(workflowId);
|
||||
return newSet;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
startingWorkflow,
|
||||
stoppingWorkflows,
|
||||
deletingWorkflows,
|
||||
startError,
|
||||
stopError,
|
||||
deleteError,
|
||||
startWorkflow,
|
||||
stopWorkflow,
|
||||
deleteWorkflow,
|
||||
isLoading
|
||||
};
|
||||
}
|
||||
|
||||
// Workflow status hook
|
||||
export function useWorkflowStatus(workflowId: string | null) {
|
||||
const [status, setStatus] = useState<Workflow | null>(null);
|
||||
const { request, isLoading: loading, error } = useApiRequest<null, Workflow>();
|
||||
|
||||
const fetchStatus = async () => {
|
||||
if (!workflowId) return;
|
||||
|
||||
try {
|
||||
const data = await request({
|
||||
url: `/api/workflows/${workflowId}/status`,
|
||||
method: 'get'
|
||||
});
|
||||
|
||||
setStatus(data);
|
||||
} catch (error) {
|
||||
// Error is already handled by useApiRequest
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchStatus();
|
||||
}, [workflowId]);
|
||||
|
||||
return { status, loading, error, refetch: fetchStatus };
|
||||
}
|
||||
|
||||
// Workflow messages hook
|
||||
export function useWorkflowMessages(workflowId: string | null, messageId?: string) {
|
||||
const [messages, setMessages] = useState<WorkflowMessage[]>([]);
|
||||
const { request, isLoading: loading, error } = useApiRequest<null, WorkflowMessage[]>();
|
||||
|
||||
const fetchMessages = async () => {
|
||||
if (!workflowId) return;
|
||||
|
||||
try {
|
||||
const data = await request({
|
||||
url: `/api/workflows/${workflowId}/messages`,
|
||||
method: 'get',
|
||||
params: messageId ? { messageId } : undefined
|
||||
});
|
||||
|
||||
setMessages(data);
|
||||
} catch (error) {
|
||||
// Error is already handled by useApiRequest
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchMessages();
|
||||
}, [workflowId, messageId]);
|
||||
|
||||
return { messages, loading, error, refetch: fetchMessages };
|
||||
}
|
||||
|
||||
// Workflow logs hook
|
||||
export function useWorkflowLogs(workflowId: string | null, logId?: string, enablePolling: boolean = false, workflowCompleted: boolean = false) {
|
||||
const [logs, setLogs] = useState<WorkflowLog[]>([]);
|
||||
const { request, isLoading: loading, error } = useApiRequest<null, WorkflowLog[]>();
|
||||
|
||||
const fetchLogs = useCallback(async () => {
|
||||
if (!workflowId) return;
|
||||
|
||||
try {
|
||||
const data = await request({
|
||||
url: `/api/workflows/${workflowId}/logs`,
|
||||
method: 'get',
|
||||
params: logId ? { logId } : undefined
|
||||
});
|
||||
|
||||
setLogs(data);
|
||||
} catch (error) {
|
||||
// Error is already handled by useApiRequest
|
||||
}
|
||||
}, [workflowId, logId, request]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchLogs();
|
||||
}, [fetchLogs]);
|
||||
|
||||
// Polling effect for real-time log updates - poll every second until workflow is completed
|
||||
useEffect(() => {
|
||||
if (!workflowId || !enablePolling || workflowCompleted) return;
|
||||
|
||||
const interval = setInterval(() => {
|
||||
fetchLogs();
|
||||
}, 1000); // Poll every second for logs
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [workflowId, enablePolling, workflowCompleted, fetchLogs]);
|
||||
|
||||
// Clear logs when workflowId changes
|
||||
useEffect(() => {
|
||||
if (!workflowId) {
|
||||
setLogs([]);
|
||||
}
|
||||
}, [workflowId]);
|
||||
|
||||
return { logs, loading, error, refetch: fetchLogs };
|
||||
}
|
||||
58
src/pages/Dashboard.module.css
Normal file
58
src/pages/Dashboard.module.css
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
.dashboardContainer {
|
||||
margin: 51px 49px 0 36px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.chatLogContainer {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.chatLogContainer.expanded {
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
/* Height classes for different states */
|
||||
.chatArea15vh {
|
||||
height: 15vh;
|
||||
}
|
||||
|
||||
.chatArea40vh {
|
||||
height: 40vh;
|
||||
}
|
||||
|
||||
.chatArea45vh {
|
||||
height: 45vh;
|
||||
}
|
||||
|
||||
.chatArea60vh {
|
||||
height: 60vh;
|
||||
}
|
||||
|
||||
.logArea15vh {
|
||||
height: 15vh;
|
||||
}
|
||||
|
||||
.logArea25vh {
|
||||
height: 25vh;
|
||||
}
|
||||
|
||||
.logArea40vh {
|
||||
height: 40vh;
|
||||
}
|
||||
|
||||
.logArea60vh {
|
||||
height: 60vh;
|
||||
}
|
||||
|
||||
.promptArea30vh {
|
||||
height: 30vh;
|
||||
}
|
||||
|
||||
.promptArea40vh {
|
||||
height: 40vh;
|
||||
}
|
||||
103
src/pages/Dashboard.tsx
Normal file
103
src/pages/Dashboard.tsx
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
import React, { useState, useCallback } from 'react';
|
||||
import DashboardPrompt from '../components/Dashboard/DashboardPrompt/DashboardPrompt';
|
||||
import DashboardChat from '../components/Dashboard/DashboardChat/DashboardChat';
|
||||
import DashboardLog from '../components/Dashboard/DashboardLog/DashboardLog';
|
||||
import { Prompt } from '../hooks/usePrompts';
|
||||
import styles from './Dashboard.module.css'
|
||||
|
||||
function Dashboard () {
|
||||
const [isChatExpanded, setIsChatExpanded] = useState(false);
|
||||
const [selectedPrompt, setSelectedPrompt] = useState<Prompt | null>(null);
|
||||
const [isPromptAreaCollapsed, setIsPromptAreaCollapsed] = useState(false);
|
||||
const [currentWorkflowId, setCurrentWorkflowId] = useState<string | null>(null);
|
||||
|
||||
const handleChatToggleExpand = () => {
|
||||
setIsChatExpanded(!isChatExpanded);
|
||||
};
|
||||
|
||||
const handlePromptRun = (prompt: Prompt) => {
|
||||
setSelectedPrompt(prompt);
|
||||
setIsPromptAreaCollapsed(true);
|
||||
};
|
||||
|
||||
const handleWorkflowIdChange = useCallback((workflowId: string | null) => {
|
||||
setCurrentWorkflowId(workflowId);
|
||||
}, []);
|
||||
|
||||
const handleWorkflowResume = useCallback((workflowId: string) => {
|
||||
// Set the workflow ID to resume it
|
||||
setCurrentWorkflowId(workflowId);
|
||||
// Switch to Chat Area tab to show the resumed workflow
|
||||
console.log('Resuming workflow:', workflowId);
|
||||
}, []);
|
||||
|
||||
// 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;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.dashboardContainer}>
|
||||
<div
|
||||
className={getPromptClass()}
|
||||
style={{
|
||||
marginBottom: !isPromptAreaCollapsed ? "40px" : "0"
|
||||
}}
|
||||
>
|
||||
<DashboardPrompt
|
||||
onPromptRun={handlePromptRun}
|
||||
isCollapsed={isPromptAreaCollapsed}
|
||||
onToggleCollapse={() => setIsPromptAreaCollapsed(!isPromptAreaCollapsed)}
|
||||
/>
|
||||
</div>
|
||||
<div className={`${styles.chatLogContainer} ${isChatExpanded ? styles.expanded : ''}`}>
|
||||
<div
|
||||
className={getChatClass()}
|
||||
style={{
|
||||
width: isChatExpanded ? "100%" : "calc(50% - 10px)",
|
||||
flex: isChatExpanded ? "none" : "1",
|
||||
marginBottom: isChatExpanded ? "40px" : "0"
|
||||
}}
|
||||
>
|
||||
<DashboardChat
|
||||
isExpanded={isChatExpanded}
|
||||
onToggleExpand={handleChatToggleExpand}
|
||||
selectedPrompt={selectedPrompt}
|
||||
onPromptUsed={() => setSelectedPrompt(null)}
|
||||
onWorkflowIdChange={handleWorkflowIdChange}
|
||||
onWorkflowResume={handleWorkflowResume}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={getLogClass()}
|
||||
style={{
|
||||
width: isChatExpanded ? "100%" : "calc(50% - 10px)",
|
||||
flex: isChatExpanded ? "none" : "1"
|
||||
}}
|
||||
>
|
||||
<DashboardLog
|
||||
isExpanded={isChatExpanded}
|
||||
workflowId={currentWorkflowId}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Dashboard;
|
||||
|
|
@ -6,7 +6,9 @@
|
|||
width: 100%;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
z-index: 0;
|
||||
overflow: hidden; /* just in case */
|
||||
overflow: hidden;
|
||||
padding: 0 49px 0 36px;
|
||||
width: calc(100% - 49px - 36px);
|
||||
}
|
||||
|
||||
.homeContainer::before {
|
||||
|
|
|
|||
Loading…
Reference in a new issue