smaller bug fixes with workflow integration

This commit is contained in:
idittrich-valueon 2025-05-28 13:56:36 +02:00
parent bc65e5a112
commit 16965f76d6
17 changed files with 951 additions and 449 deletions

View file

@ -14,6 +14,7 @@ interface DashboardChatProps {
selectedPrompt?: Prompt | null;
onPromptUsed?: () => void;
onWorkflowIdChange?: (workflowId: string | null) => void;
onWorkflowCompletedChange?: (completed: boolean) => void;
onWorkflowResume?: (workflowId: string) => void;
}
@ -23,6 +24,7 @@ const DashboardChat: React.FC<DashboardChatProps> = ({
selectedPrompt,
onPromptUsed,
onWorkflowIdChange,
onWorkflowCompletedChange,
onWorkflowResume
}) => {
const [activeTab, setActiveTab] = useState("Chat Area");
@ -119,6 +121,7 @@ const DashboardChat: React.FC<DashboardChatProps> = ({
selectedPrompt={selectedPrompt}
onPromptUsed={onPromptUsed}
onWorkflowIdChange={onWorkflowIdChange}
onWorkflowCompletedChange={onWorkflowCompletedChange}
resumeWorkflowId={resumeWorkflowId}
/>
) : (

View file

@ -0,0 +1,38 @@
import React from "react";
import { WorkflowStatusDisplayProps } from "./dashboardChatAreaTypes";
import styles from './DashboardChatArea.module.css';
const WorkflowStatusDisplay: React.FC<WorkflowStatusDisplayProps> = ({
currentWorkflowId,
workflowStatus,
workflowCompleted,
onStartNewWorkflow
}) => {
if (!currentWorkflowId) return null;
return (
<>
{!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={onStartNewWorkflow}
>
Start New Workflow
</button>
</div>
)}
</>
);
};
export default WorkflowStatusDisplay;

View file

@ -94,6 +94,31 @@
opacity: 0.6;
}
.stop_button {
padding: 12px 12px;
background-color: #D85B65;
color: white;
border: none;
border-radius: 12px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.2s ease;
}
.stop_button:hover {
background-color: #c3525b;
}
.stop_button:disabled {
background-color: #ccc;
cursor: not-allowed;
opacity: 0.6;
}
.loading_message {
padding: 10px;
background-color: #e3f2fd;

View file

@ -1,276 +1,87 @@
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 React, { useEffect } from "react";
import { DashboardChatAreaProps } from "./dashboardChatAreaTypes";
import { useChatLogic } from "./dashboardChatAreaLogic";
import MessageList from "./DashboardChatAreaMessageList";
import ChatInput from "./DashboardChatAreaInput";
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,
onWorkflowCompletedChange,
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);
const {
// State
inputValue,
setInputValue,
currentWorkflowId,
workflowCompleted,
// Refs
inputRef,
messagesEndRef,
// Data from hooks
messages,
messagesLoading,
messagesError,
startingWorkflow,
startError,
workflowStatus,
// Handlers
handleSend,
handleKeyPress,
startNewWorkflow,
handleStopWorkflow,
// Workflow state
isWorkflowRunning,
isStoppingWorkflow
} = useChatLogic({
selectedPrompt,
onPromptUsed,
onWorkflowIdChange,
resumeWorkflowId
});
// Update input value when a prompt is selected
// Notify parent component when workflow completion status changes
useEffect(() => {
if (selectedPrompt) {
setInputValue(selectedPrompt.content);
// Focus the input field
if (inputRef.current) {
inputRef.current.focus();
}
if (onWorkflowCompletedChange) {
onWorkflowCompletedChange(workflowCompleted);
}
}, [selectedPrompt]);
}, [workflowCompleted, onWorkflowCompletedChange]);
// 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]);
const placeholder = workflowCompleted ? "Continue the conversation..." : "Type your message...";
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>
<MessageList
messages={messages}
currentWorkflowId={currentWorkflowId}
workflowStatus={workflowStatus}
workflowCompleted={workflowCompleted}
startingWorkflow={startingWorkflow}
startError={startError}
messagesError={messagesError}
messagesLoading={messagesLoading}
onStartNewWorkflow={startNewWorkflow}
messagesEndRef={messagesEndRef}
/>
<ChatInput
inputValue={inputValue}
setInputValue={setInputValue}
onSend={handleSend}
onKeyPress={handleKeyPress}
isDisabled={startingWorkflow}
placeholder={placeholder}
inputRef={inputRef}
isWorkflowRunning={isWorkflowRunning}
onStopWorkflow={handleStopWorkflow}
isStoppingWorkflow={isStoppingWorkflow}
/>
</div>
);
};

View file

@ -0,0 +1,54 @@
import React from "react";
import { motion } from "framer-motion";
import { LuSendHorizontal } from "react-icons/lu";
import { FaStop } from "react-icons/fa";
import { ChatInputProps } from "./dashboardChatAreaTypes";
import styles from './DashboardChatArea.module.css';
const ChatInput: React.FC<ChatInputProps> = ({
inputValue,
setInputValue,
onSend,
onKeyPress,
isDisabled,
placeholder,
inputRef,
isWorkflowRunning,
onStopWorkflow,
isStoppingWorkflow
}) => {
return (
<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={onKeyPress}
placeholder={placeholder}
className={styles.message_input}
disabled={isDisabled}
/>
<motion.button
className={isWorkflowRunning ? styles.stop_button : styles.send_button}
onClick={isWorkflowRunning ? onStopWorkflow : onSend}
disabled={isWorkflowRunning ? isStoppingWorkflow : (isDisabled || !inputValue.trim())}
whileTap={{ scale: 0.95 }}
transition={{ duration: 0.2, ease: "easeOut" }}
>
{isWorkflowRunning ? (
<FaStop className={styles.send_button_icon}/>
) : (
<LuSendHorizontal className={styles.send_button_icon}/>
)}
</motion.button>
</motion.div>
);
};
export default ChatInput;

View file

@ -0,0 +1,32 @@
import React from "react";
import { Message } from "./dashboardChatAreaTypes";
import styles from './DashboardChatArea.module.css';
interface MessageItemProps {
message: Message;
index: number;
}
const MessageItem: React.FC<MessageItemProps> = ({ message, index }) => {
return (
<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>
);
};
export default MessageItem;

View file

@ -0,0 +1,73 @@
import React from "react";
import { motion } from "framer-motion";
import { MessageListProps } from "./dashboardChatAreaTypes";
import MessageItem from "./DashboardChatAreaMessageItem";
import WorkflowStatusDisplay from "./DashbaordChatAreaStatusDisplay";
import styles from './DashboardChatArea.module.css';
const MessageList: React.FC<MessageListProps> = ({
messages,
currentWorkflowId,
workflowStatus,
workflowCompleted,
startingWorkflow,
startError,
messagesError,
messagesLoading,
onStartNewWorkflow,
messagesEndRef
}) => {
return (
<motion.div
className={styles.chat_messages}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.2, duration: 0.3, ease: "easeOut" }}
>
<div className={styles.messages_container}>
{startingWorkflow && (
<div className={styles.loading_message}>
<p>{workflowCompleted && currentWorkflowId ? '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) => (
<MessageItem
key={message.id || index}
message={message}
index={index}
/>
))
) : !currentWorkflowId ? (
<p className={styles.placeholder_text}>Start a conversation by typing a message...</p>
) : null}
<WorkflowStatusDisplay
currentWorkflowId={currentWorkflowId}
workflowStatus={workflowStatus}
workflowCompleted={workflowCompleted}
onStartNewWorkflow={onStartNewWorkflow}
/>
<div ref={messagesEndRef} />
</div>
</motion.div>
);
};
export default MessageList;

View file

@ -0,0 +1,109 @@
# DashboardChatArea - Modular Structure
This directory contains the refactored `DashboardChatArea` component, broken down into manageable modules for better maintainability and separation of concerns.
## File Structure
```
DashboardChatArea/
├── index.ts # Main export file
├── types.ts # TypeScript interfaces and types
├── DashboardChatArea.tsx # Main orchestrating component
├── useChatLogic.ts # Custom hook with all business logic
├── MessageList.tsx # Component for displaying messages
├── MessageItem.tsx # Individual message component
├── ChatInput.tsx # Input field and send button component
├── WorkflowStatusDisplay.tsx # Workflow status and completion UI
├── DashboardChatArea.module.css # Shared styles
└── README.md # This documentation
```
## Component Responsibilities
### `DashboardChatArea.tsx` (Main Component)
- **Purpose**: Orchestrates all child components
- **Responsibilities**:
- Uses the `useChatLogic` hook
- Renders `MessageList` and `ChatInput` components
- Passes props between components
- **Size**: ~73 lines (reduced from 278 lines)
### `useChatLogic.ts` (Custom Hook)
- **Purpose**: Contains all business logic and state management
- **Responsibilities**:
- State management (input value, workflow ID, completion status)
- Effects for polling, auto-scroll, prompt handling
- Workflow operations (send messages, start workflows)
- Event handlers
- **Size**: ~196 lines
### `MessageList.tsx` (Message Display)
- **Purpose**: Handles the display of all messages and status indicators
- **Responsibilities**:
- Renders loading and error states
- Maps through messages using `MessageItem`
- Includes `WorkflowStatusDisplay`
- Handles auto-scroll reference
- **Size**: ~73 lines
### `MessageItem.tsx` (Individual Message)
- **Purpose**: Renders a single message
- **Responsibilities**:
- Message content display
- Role-based styling
- Timestamp formatting
- **Size**: ~32 lines
### `ChatInput.tsx` (Input Interface)
- **Purpose**: Handles user input and send functionality
- **Responsibilities**:
- Input field with ref handling
- Send button with animations
- Keyboard event handling
- Disabled states
- **Size**: ~46 lines
### `WorkflowStatusDisplay.tsx` (Status UI)
- **Purpose**: Shows workflow status and completion states
- **Responsibilities**:
- Running workflow status
- Completion message
- "Start New Workflow" button
- **Size**: ~38 lines
### `types.ts` (Type Definitions)
- **Purpose**: Centralized TypeScript interfaces
- **Responsibilities**:
- Component prop interfaces
- Data structure types
- Shared type definitions
- **Size**: ~50 lines
## Benefits of This Structure
1. **Separation of Concerns**: Each file has a single, clear responsibility
2. **Reusability**: Components can be easily reused or tested independently
3. **Maintainability**: Easier to locate and modify specific functionality
4. **Readability**: Smaller files are easier to understand and navigate
5. **Testing**: Individual components can be unit tested in isolation
6. **Type Safety**: Centralized types ensure consistency across components
## Usage
Import the main component as before:
```typescript
import DashboardChatArea from './DashboardChatArea';
// or
import DashboardChatArea, { DashboardChatAreaProps } from './DashboardChatArea';
```
The API remains exactly the same - this refactoring is purely internal and doesn't affect how the component is used by parent components.
## Development Guidelines
- **Adding new features**: Consider which component/file is most appropriate
- **State changes**: Most state logic should go in `useChatLogic.ts`
- **UI changes**: Modify the relevant component file
- **New types**: Add to `types.ts`
- **Styling**: All styles remain in `DashboardChatArea.module.css`

View file

@ -0,0 +1,213 @@
import { useState, useEffect, useRef } from "react";
import { Prompt } from "../../../../hooks/usePrompts";
import { useWorkflowOperations, useWorkflowMessages, useWorkflowStatus } from "../../../../hooks/useWorkflows";
interface UseChatLogicProps {
selectedPrompt?: Prompt | null;
onPromptUsed?: () => void;
onWorkflowIdChange?: (workflowId: string | null) => void;
resumeWorkflowId?: string | null;
}
export const useChatLogic = ({
selectedPrompt,
onPromptUsed,
onWorkflowIdChange,
resumeWorkflowId
}: UseChatLogicProps) => {
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, stopWorkflow, stoppingWorkflows } = 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]);
// Simple workflow completion detection
useEffect(() => {
const isCompleted = workflowStatus && (
workflowStatus.status === 'completed' ||
workflowStatus.status === 'finished' ||
workflowStatus.status === 'done' ||
workflowStatus.status === 'stopped' ||
workflowStatus.status === 'error'
);
setWorkflowCompleted(!!isCompleted);
}, [workflowStatus]);
// Handle workflow ID changes
useEffect(() => {
if (currentWorkflowId && onWorkflowIdChange) {
onWorkflowIdChange(currentWorkflowId);
}
}, [currentWorkflowId, onWorkflowIdChange]);
// Handle workflow resumption
useEffect(() => {
if (resumeWorkflowId && resumeWorkflowId !== currentWorkflowId) {
setCurrentWorkflowId(resumeWorkflowId);
setWorkflowCompleted(false);
setInputValue("");
}
}, [resumeWorkflowId, currentWorkflowId]);
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 for messages/status
// but DON'T reset logPollingCompleted - logs should stay stopped
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);
}
};
const handleStopWorkflow = async () => {
if (currentWorkflowId) {
console.log('Stopping workflow:', currentWorkflowId);
const success = await stopWorkflow(currentWorkflowId);
if (success) {
console.log('Workflow stopped successfully');
// Refresh status to get updated workflow state
refetchStatus();
} else {
console.error('Failed to stop workflow');
}
}
};
// Determine if workflow is currently running
const isWorkflowRunning = !!(currentWorkflowId && !workflowCompleted && workflowStatus &&
workflowStatus.status !== 'completed' &&
workflowStatus.status !== 'finished' &&
workflowStatus.status !== 'done' &&
workflowStatus.status !== 'stopped' &&
workflowStatus.status !== 'error');
const isStoppingWorkflow = currentWorkflowId ? stoppingWorkflows.has(currentWorkflowId) : false;
return {
// State
inputValue,
setInputValue,
currentWorkflowId,
workflowCompleted,
// Refs
inputRef,
messagesEndRef,
// Data from hooks
messages,
messagesLoading,
messagesError,
startingWorkflow,
startError,
workflowStatus,
// Handlers
handleSend,
handleKeyPress,
startNewWorkflow,
handleStopWorkflow,
// Workflow state
isWorkflowRunning,
isStoppingWorkflow
};
};

View file

@ -0,0 +1,54 @@
import { Prompt } from "../../../../hooks/usePrompts";
export interface DashboardChatAreaProps {
selectedPrompt?: Prompt | null;
onPromptUsed?: () => void;
onWorkflowIdChange?: (workflowId: string | null) => void;
onWorkflowCompletedChange?: (completed: boolean) => void;
resumeWorkflowId?: string | null;
}
export interface Message {
id?: string;
role: 'user' | 'assistant' | 'system';
content: string;
timestamp?: string;
}
export interface WorkflowStatus {
status: string;
currentRound?: number;
}
export interface ChatInputProps {
inputValue: string;
setInputValue: (value: string) => void;
onSend: () => void;
onKeyPress: (e: React.KeyboardEvent) => void;
isDisabled: boolean;
placeholder: string;
inputRef: React.RefObject<HTMLInputElement | null>;
isWorkflowRunning: boolean;
onStopWorkflow: () => void;
isStoppingWorkflow: boolean;
}
export interface MessageListProps {
messages: Message[];
currentWorkflowId: string | null;
workflowStatus: WorkflowStatus | null;
workflowCompleted: boolean;
startingWorkflow: boolean;
startError: string | null;
messagesError: string | null;
messagesLoading: boolean;
onStartNewWorkflow: () => void;
messagesEndRef: React.RefObject<HTMLDivElement | null>;
}
export interface WorkflowStatusDisplayProps {
currentWorkflowId: string | null;
workflowStatus: WorkflowStatus | null;
workflowCompleted: boolean;
onStartNewWorkflow: () => void;
}

View file

@ -0,0 +1,2 @@
export { default } from './DashboardChatArea';
export type { DashboardChatAreaProps } from './dashboardChatAreaTypes';

View file

@ -48,13 +48,13 @@
font-weight: 600;
padding: 4px 8px;
border-radius: 12px;
background-color: rgba(0, 0, 0, 0.05);
background-color: #f4f3f5;
}
.workflowRound {
font-size: 12px;
color: #666;
background-color: #f5f5f5;
color: #888098;
background-color: #f4f3f5;
padding: 2px 6px;
border-radius: 8px;
}
@ -79,24 +79,15 @@
.messagePreview {
margin-bottom: 8px;
padding: 8px;
background-color: #f8f9fa;
background-color: #f4f3f5;
border-radius: 6px;
border-left: 3px solid #2196F3;
border-left: 3px solid #888098;
}
.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;
color: #888098;
margin: 0;
line-height: 1.4;
word-break: break-word;
@ -137,22 +128,22 @@
}
.resumeButton {
background-color: #4CAF50;
background-color: #3a8088;
color: white;
}
.resumeButton:hover:not(:disabled) {
background-color: #45a049;
background-color: #34737b;
transform: translateY(-1px);
}
.deleteButton {
background-color: #f44336;
background-color: #d85b65;
color: white;
}
.deleteButton:hover:not(:disabled) {
background-color: #da190b;
background-color: #c3525b;
transform: translateY(-1px);
}

View file

@ -53,18 +53,37 @@ function DashboardChatHistoryItem({ workflow, onDelete, onResume }: DashboardCha
case 'completed':
case 'finished':
case 'done':
return '#4CAF50';
return '#3a8088';
case 'running':
case 'processing':
return '#2196F3';
return '#888098';
case 'error':
case 'failed':
return '#F44336';
return '#d85d67';
case 'stopped':
case 'cancelled':
return '#FF9800';
return '#d85d67';
default:
return '#9E9E9E';
return '#888098';
}
};
const getStatusBackgroundColor = (status: string) => {
switch (status.toLowerCase()) {
case 'completed':
case 'finished':
case 'done':
return '#e6f2f2';
case 'running':
case 'processing':
return '#f0f0f5';
case 'error':
case 'failed':
case 'stopped':
case 'cancelled':
return '#fceff0';
default:
return '#f0f0f5';
}
};
@ -84,7 +103,10 @@ function DashboardChatHistoryItem({ workflow, onDelete, onResume }: DashboardCha
<div className={styles.workflowMeta}>
<span
className={styles.workflowStatus}
style={{ color: getStatusColor(workflow.status) }}
style={{
color: getStatusColor(workflow.status),
backgroundColor: getStatusBackgroundColor(workflow.status)
}}
>
{workflow.status.toUpperCase()}
</span>
@ -110,16 +132,11 @@ function DashboardChatHistoryItem({ workflow, onDelete, onResume }: DashboardCha
<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>

View file

@ -87,21 +87,23 @@
}
.log_entries {
background-color: #f9f9f9;
border-radius: 15px;
padding: 15px;
height: 100%;
overflow-y: auto;
font-family: 'Courier New', monospace;
display: flex;
flex-direction: column;
gap: 4px;
scroll-behavior: smooth;
}
.log_entries::-webkit-scrollbar {
display: none;
}
.log_entry {
display: flex;
gap: 15px;
padding: 8px 0;
border-bottom: 1px solid #e0e0e0;
align-items: center;
font-size: 12px;
align-items: flex-start;
gap: 8px;
padding: 4px 0;
border-bottom: 1px solid rgba(0, 255, 0, 0.1);
animation: fadeIn 0.3s ease-in;
}
.log_entry:last-child {
@ -109,9 +111,25 @@
}
.log_timestamp {
color: #666;
min-width: 140px;
font-weight: 500;
color: #888;
font-size: 11px;
min-width: 80px;
font-weight: bold;
}
.log_level {
color: #00aaff;
font-size: 11px;
font-weight: bold;
min-width: 60px;
}
.log_message {
color: #00ff00;
font-size: 12px;
flex: 1;
word-break: break-word;
line-height: 1.3;
}
.log_level_info {
@ -147,18 +165,15 @@
text-align: center;
}
.log_message {
flex: 1;
color: #333;
}
/* Hacker-style console styles */
.console_container {
background-color: #0a0a0a;
border-radius: 8px;
border-radius: 15px;
height: 100%;
padding: 15px;
flex: 1;
min-height: 200px;
min-height: 0;
max-height: 100%;
overflow-y: auto;
font-family: 'Courier New', 'Monaco', 'Menlo', monospace;
font-size: 12px;
@ -305,3 +320,22 @@
opacity: 0;
}
}
/* Live indicator styling */
.live_indicator {
color: #00ff00;
font-size: 12px;
font-weight: bold;
animation: pulse 1.5s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.7;
transform: scale(1.05);
}
}

View file

@ -1,66 +1,163 @@
import React, { useEffect, useRef } from "react";
import React, { useState, useEffect, useRef } from "react";
import { motion } from "framer-motion";
import { useWorkflowLogs, useWorkflowStatus } from "../../../hooks/useWorkflows";
import styles from './DashboardLog.module.css';
import { useApiRequest } from '../../../hooks/useApi';
interface DashboardLogProps {
isExpanded: boolean;
workflowId: string | null;
workflowCompleted?: boolean;
}
const DashboardLog: React.FC<DashboardLogProps> = ({ isExpanded, workflowId }) => {
const { status: workflowStatus } = useWorkflowStatus(workflowId);
const DashboardLog: React.FC<DashboardLogProps> = ({ isExpanded, workflowId, workflowCompleted = false }) => {
const [logs, setLogs] = useState<any[]>([]);
const [isPolling, setIsPolling] = useState(false);
const [logsError, setLogsError] = useState<string | null>(null);
const intervalRef = useRef<number | null>(null);
const consoleContainerRef = useRef<HTMLDivElement>(null);
// 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);
const { request, isLoading: logsLoading } = useApiRequest<null, any[]>();
// 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';
// Function to fetch logs directly
const fetchLogs = async (workflowIdToFetch: string) => {
try {
return new Date(timestamp).toLocaleTimeString('en-US', {
hour12: false,
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
console.log('Fetching logs for workflow:', workflowIdToFetch);
const data = await request({
url: `/api/workflows/${workflowIdToFetch}/logs`,
method: 'get'
});
} catch (error) {
return '00:00:00';
console.log('Logs fetched:', data);
setLogs(data || []);
setLogsError(null);
} catch (error: any) {
console.error('Error fetching logs:', error);
setLogsError(error.message || 'Failed to fetch logs');
}
};
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';
// Auto-scroll to bottom when new logs arrive during polling
useEffect(() => {
if (isPolling && consoleContainerRef.current && logs.length > 0) {
consoleContainerRef.current.scrollTop = consoleContainerRef.current.scrollHeight;
}
}, [logs, isPolling]);
// Start/stop log polling based on workflow state
useEffect(() => {
console.log('Log polling effect triggered:', { workflowId, workflowCompleted, isPolling });
// Clear any existing interval
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
if (!workflowId) {
// No workflow - stop everything
console.log('No workflow ID, stopping polling');
setIsPolling(false);
setLogs([]);
setLogsError(null);
return;
}
if (workflowCompleted) {
// Workflow completed - stop polling but fetch final logs
console.log('Workflow completed, stopping polling but fetching final logs');
setIsPolling(false);
fetchLogs(workflowId);
} else {
// Workflow is running - start polling immediately
console.log('Workflow running, starting polling');
setIsPolling(true);
// Fetch logs immediately
fetchLogs(workflowId);
// Start polling every second
intervalRef.current = setInterval(() => {
console.log('Polling for logs...');
fetchLogs(workflowId);
}, 1000);
}
// Cleanup on unmount or dependency change
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
};
}, [workflowId, workflowCompleted]);
// Debug logs data
useEffect(() => {
console.log('Logs data updated:', {
logsCount: logs.length,
logs: logs.slice(0, 3), // Show first 3 logs
logsLoading,
logsError,
isPolling
});
}, [logs, logsLoading, logsError, isPolling]);
const renderLogContent = () => {
if (!workflowId) {
return (
<div className={styles.console_placeholder}>
<span className={styles.console_prompt}>$</span>
<span className={styles.console_text}>No workflow selected</span>
</div>
);
}
if (logsLoading && logs.length === 0) {
return (
<div className={styles.console_placeholder}>
<span className={styles.console_prompt}>$</span>
<span className={styles.console_text}>Loading logs...</span>
</div>
);
}
if (logsError) {
return (
<div className={styles.console_placeholder}>
<span className={styles.console_prompt}>$</span>
<span className={styles.console_text}>Error loading logs: {logsError}</span>
</div>
);
}
if (logs.length === 0) {
const statusText = workflowCompleted
? "No logs available for this workflow"
: "Workflow running... Waiting for logs...";
return (
<div className={styles.console_placeholder}>
<span className={styles.console_prompt}>$</span>
<span className={styles.console_text}>{statusText}</span>
{isPolling && <span className={styles.console_cursor}>|</span>}
</div>
);
}
return (
<div className={styles.log_entries}>
{logs.map((log, index) => (
<div key={log.id || index} className={styles.log_entry}>
<span className={styles.log_timestamp}>
{log.timestamp ? new Date(log.timestamp).toLocaleTimeString() : ''}
</span>
<span className={styles.log_level}>[{log.level || 'INFO'}]</span>
<span className={styles.log_message}>{log.message || log.content || JSON.stringify(log)}</span>
</div>
))}
</div>
);
};
return (
@ -103,53 +200,9 @@ const DashboardLog: React.FC<DashboardLogProps> = ({ isExpanded, workflowId }) =
animate={{ opacity: 1, height: "auto" }}
transition={{ duration: 0.4, ease: "easeOut" }}
>
<div className={styles.console_container}>
<div className={styles.console_container} ref={consoleContainerRef}>
<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} />
{renderLogContent()}
</div>
</div>
</motion.div>

View file

@ -1,4 +1,4 @@
import { useState, useEffect, useCallback } from 'react';
import { useState, useEffect, useCallback, useRef } from 'react';
import { useApiRequest } from './useApi';
// Workflow interfaces
@ -23,14 +23,6 @@ export interface WorkflowMessage {
fileIds?: number[];
}
export interface WorkflowLog {
id: string;
level: string;
message: string;
timestamp: string;
data?: Record<string, any>;
}
export interface StartWorkflowRequest {
prompt: string;
listFileId: number[];
@ -214,11 +206,11 @@ export function useWorkflowMessages(workflowId: string | null, messageId?: strin
}
// 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[]>();
export function useWorkflowLogs(workflowId: string | null, logId?: string) {
const [logs, setLogs] = useState<any[]>([]);
const { request, isLoading: loading, error } = useApiRequest<null, any[]>();
const fetchLogs = useCallback(async () => {
const fetchLogs = async () => {
if (!workflowId) return;
try {
@ -232,29 +224,11 @@ export function useWorkflowLogs(workflowId: string | null, logId?: string, enabl
} 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]);
}, [workflowId, logId]);
return { logs, loading, error, refetch: fetchLogs };
}

View file

@ -1,4 +1,4 @@
import React, { useState, useCallback } from 'react';
import React, { useState, useCallback, useMemo } from 'react';
import DashboardPrompt from '../components/Dashboard/DashboardPrompt/DashboardPrompt';
import DashboardChat from '../components/Dashboard/DashboardChat/DashboardChat';
import DashboardLog from '../components/Dashboard/DashboardLog/DashboardLog';
@ -10,6 +10,7 @@ function Dashboard () {
const [selectedPrompt, setSelectedPrompt] = useState<Prompt | null>(null);
const [isPromptAreaCollapsed, setIsPromptAreaCollapsed] = useState(false);
const [currentWorkflowId, setCurrentWorkflowId] = useState<string | null>(null);
const [workflowCompleted, setWorkflowCompleted] = useState(false);
const handleChatToggleExpand = () => {
setIsChatExpanded(!isChatExpanded);
@ -22,13 +23,22 @@ function Dashboard () {
const handleWorkflowIdChange = useCallback((workflowId: string | null) => {
setCurrentWorkflowId(workflowId);
// Reset completion status when workflow changes
if (workflowId !== currentWorkflowId) {
setWorkflowCompleted(false);
}
}, [currentWorkflowId]);
const handleWorkflowCompletedChange = useCallback((completed: boolean) => {
setWorkflowCompleted(completed);
}, []);
const handleWorkflowResume = useCallback((workflowId: string) => {
// Set the workflow ID to resume it
setCurrentWorkflowId(workflowId);
// Reset completion status when resuming
setWorkflowCompleted(false);
// Switch to Chat Area tab to show the resumed workflow
console.log('Resuming workflow:', workflowId);
}, []);
// Determine CSS classes based on states
@ -51,13 +61,27 @@ function Dashboard () {
return styles.logArea40vh;
};
// Memoize style objects to prevent infinite re-renders
const promptStyle = useMemo(() => ({
marginBottom: !isPromptAreaCollapsed ? "40px" : "0"
}), [isPromptAreaCollapsed]);
const chatStyle = useMemo(() => ({
width: isChatExpanded ? "100%" : "calc(50% - 10px)",
flex: isChatExpanded ? "none" : "1",
marginBottom: isChatExpanded ? "40px" : "0"
}), [isChatExpanded]);
const logStyle = useMemo(() => ({
width: isChatExpanded ? "100%" : "calc(50% - 10px)",
flex: isChatExpanded ? "none" : "1"
}), [isChatExpanded]);
return (
<div className={styles.dashboardContainer}>
<div
className={getPromptClass()}
style={{
marginBottom: !isPromptAreaCollapsed ? "40px" : "0"
}}
style={promptStyle}
>
<DashboardPrompt
onPromptRun={handlePromptRun}
@ -68,11 +92,7 @@ function Dashboard () {
<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"
}}
style={chatStyle}
>
<DashboardChat
isExpanded={isChatExpanded}
@ -80,19 +100,18 @@ function Dashboard () {
selectedPrompt={selectedPrompt}
onPromptUsed={() => setSelectedPrompt(null)}
onWorkflowIdChange={handleWorkflowIdChange}
onWorkflowCompletedChange={handleWorkflowCompletedChange}
onWorkflowResume={handleWorkflowResume}
/>
</div>
<div
className={getLogClass()}
style={{
width: isChatExpanded ? "100%" : "calc(50% - 10px)",
flex: isChatExpanded ? "none" : "1"
}}
style={logStyle}
>
<DashboardLog
isExpanded={isChatExpanded}
workflowId={currentWorkflowId}
workflowCompleted={workflowCompleted}
/>
</div>
</div>