integrated workflows

This commit is contained in:
idittrich-valueon 2025-05-28 09:48:06 +02:00
parent 4db660d247
commit bc65e5a112
28 changed files with 3705 additions and 477 deletions

903
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -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",

View file

@ -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>

View file

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

View file

@ -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;
}

View 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;

View file

@ -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;
}

View file

@ -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;

View file

@ -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;
}

View file

@ -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;

View file

@ -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;
}
}

View file

@ -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;

View 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;
}
}

View 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;

View file

@ -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;
}

View 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;

View file

@ -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;
}

View file

@ -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;

View file

@ -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;
}

View file

@ -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;

View file

@ -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;
}

View file

@ -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;

View file

@ -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
View 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
View 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 };
}

View 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
View 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;

View file

@ -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 {