diff --git a/src/components/Dashboard/DashboardChat/DashboardChat.tsx b/src/components/Dashboard/DashboardChat/DashboardChat.tsx index 743e0d6..fff930a 100644 --- a/src/components/Dashboard/DashboardChat/DashboardChat.tsx +++ b/src/components/Dashboard/DashboardChat/DashboardChat.tsx @@ -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 = ({ selectedPrompt, onPromptUsed, onWorkflowIdChange, + onWorkflowCompletedChange, onWorkflowResume }) => { const [activeTab, setActiveTab] = useState("Chat Area"); @@ -119,6 +121,7 @@ const DashboardChat: React.FC = ({ selectedPrompt={selectedPrompt} onPromptUsed={onPromptUsed} onWorkflowIdChange={onWorkflowIdChange} + onWorkflowCompletedChange={onWorkflowCompletedChange} resumeWorkflowId={resumeWorkflowId} /> ) : ( diff --git a/src/components/Dashboard/DashboardChat/DashboardChatArea/DashbaordChatAreaStatusDisplay.tsx b/src/components/Dashboard/DashboardChat/DashboardChatArea/DashbaordChatAreaStatusDisplay.tsx new file mode 100644 index 0000000..f6520a1 --- /dev/null +++ b/src/components/Dashboard/DashboardChat/DashboardChatArea/DashbaordChatAreaStatusDisplay.tsx @@ -0,0 +1,38 @@ +import React from "react"; +import { WorkflowStatusDisplayProps } from "./dashboardChatAreaTypes"; +import styles from './DashboardChatArea.module.css'; + +const WorkflowStatusDisplay: React.FC = ({ + currentWorkflowId, + workflowStatus, + workflowCompleted, + onStartNewWorkflow +}) => { + if (!currentWorkflowId) return null; + + return ( + <> + {!workflowCompleted && ( +
+

+ Workflow {currentWorkflowId.substring(0, 8)}... is {workflowStatus?.status || 'running'} + {workflowStatus?.currentRound && ` (Round ${workflowStatus.currentRound})`} +

+
+ )} + {workflowCompleted && ( +
+

Workflow completed! You can continue the conversation or start a new workflow.

+ +
+ )} + + ); +}; + +export default WorkflowStatusDisplay; \ No newline at end of file diff --git a/src/components/Dashboard/DashboardChat/DashboardChatArea/DashboardChatArea.module.css b/src/components/Dashboard/DashboardChat/DashboardChatArea/DashboardChatArea.module.css index 3415626..595f25a 100644 --- a/src/components/Dashboard/DashboardChat/DashboardChatArea/DashboardChatArea.module.css +++ b/src/components/Dashboard/DashboardChat/DashboardChatArea/DashboardChatArea.module.css @@ -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; diff --git a/src/components/Dashboard/DashboardChat/DashboardChatArea/DashboardChatArea.tsx b/src/components/Dashboard/DashboardChat/DashboardChatArea/DashboardChatArea.tsx index f6057f3..7829c95 100644 --- a/src/components/Dashboard/DashboardChat/DashboardChatArea/DashboardChatArea.tsx +++ b/src/components/Dashboard/DashboardChat/DashboardChatArea/DashboardChatArea.tsx @@ -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 = ({ selectedPrompt, onPromptUsed, onWorkflowIdChange, + onWorkflowCompletedChange, resumeWorkflowId }) => { - const [inputValue, setInputValue] = useState(""); - const [currentWorkflowId, setCurrentWorkflowId] = useState(null); - const [workflowCompleted, setWorkflowCompleted] = useState(false); - const inputRef = useRef(null); - const messagesEndRef = useRef(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 (
- -
- {startingWorkflow && ( -
-

{workflowCompleted && currentWorkflowId ? 'Sending follow-up message...' : 'Sending message...'}

-
- )} - {startError && ( -
-

Error: {startError}

-
- )} - {messagesError && ( -
-

Error loading messages: {messagesError}

-
- )} - {currentWorkflowId && messagesLoading && messages.length === 0 && ( -
-

Loading workflow messages...

-
- )} - {messages.length > 0 ? ( - messages.map((message, index) => ( -
-
- {message.role === 'user' ? 'You' : - message.role === 'assistant' ? 'Assistant' : 'System'} -
-
- {message.content} -
- {message.timestamp && ( -
- {new Date(message.timestamp).toLocaleTimeString()} -
- )} -
- )) - ) : !currentWorkflowId ? ( -

Start a conversation by typing a message...

- ) : null} - {currentWorkflowId && !workflowCompleted && ( -
-

- Workflow {currentWorkflowId.substring(0, 8)}... is {workflowStatus?.status || 'running'} - {workflowStatus?.currentRound && ` (Round ${workflowStatus.currentRound})`} -

-
- )} - {workflowCompleted && ( -
-

Workflow completed! You can continue the conversation or start a new workflow.

- -
- )} -
-
- - - setInputValue(e.target.value)} - onKeyPress={handleKeyPress} - placeholder={workflowCompleted ? "Continue the conversation..." : "Type your message..."} - className={styles.message_input} - disabled={startingWorkflow} - /> - - - - + +
); }; diff --git a/src/components/Dashboard/DashboardChat/DashboardChatArea/DashboardChatAreaInput.tsx b/src/components/Dashboard/DashboardChat/DashboardChatArea/DashboardChatAreaInput.tsx new file mode 100644 index 0000000..eb628f0 --- /dev/null +++ b/src/components/Dashboard/DashboardChat/DashboardChatArea/DashboardChatAreaInput.tsx @@ -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 = ({ + inputValue, + setInputValue, + onSend, + onKeyPress, + isDisabled, + placeholder, + inputRef, + isWorkflowRunning, + onStopWorkflow, + isStoppingWorkflow +}) => { + return ( + + setInputValue(e.target.value)} + onKeyPress={onKeyPress} + placeholder={placeholder} + className={styles.message_input} + disabled={isDisabled} + /> + + {isWorkflowRunning ? ( + + ) : ( + + )} + + + ); +}; + +export default ChatInput; \ No newline at end of file diff --git a/src/components/Dashboard/DashboardChat/DashboardChatArea/DashboardChatAreaMessageItem.tsx b/src/components/Dashboard/DashboardChat/DashboardChatArea/DashboardChatAreaMessageItem.tsx new file mode 100644 index 0000000..b963beb --- /dev/null +++ b/src/components/Dashboard/DashboardChat/DashboardChatArea/DashboardChatAreaMessageItem.tsx @@ -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 = ({ message, index }) => { + return ( +
+
+ {message.role === 'user' ? 'You' : + message.role === 'assistant' ? 'Assistant' : 'System'} +
+
+ {message.content} +
+ {message.timestamp && ( +
+ {new Date(message.timestamp).toLocaleTimeString()} +
+ )} +
+ ); +}; + +export default MessageItem; \ No newline at end of file diff --git a/src/components/Dashboard/DashboardChat/DashboardChatArea/DashboardChatAreaMessageList.tsx b/src/components/Dashboard/DashboardChat/DashboardChatArea/DashboardChatAreaMessageList.tsx new file mode 100644 index 0000000..831d3ef --- /dev/null +++ b/src/components/Dashboard/DashboardChat/DashboardChatArea/DashboardChatAreaMessageList.tsx @@ -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 = ({ + messages, + currentWorkflowId, + workflowStatus, + workflowCompleted, + startingWorkflow, + startError, + messagesError, + messagesLoading, + onStartNewWorkflow, + messagesEndRef +}) => { + return ( + +
+ {startingWorkflow && ( +
+

{workflowCompleted && currentWorkflowId ? 'Sending follow-up message...' : 'Sending message...'}

+
+ )} + {startError && ( +
+

Error: {startError}

+
+ )} + {messagesError && ( +
+

Error loading messages: {messagesError}

+
+ )} + {currentWorkflowId && messagesLoading && messages.length === 0 && ( +
+

Loading workflow messages...

+
+ )} + {messages.length > 0 ? ( + messages.map((message, index) => ( + + )) + ) : !currentWorkflowId ? ( +

Start a conversation by typing a message...

+ ) : null} + + + +
+
+ + ); +}; + +export default MessageList; \ No newline at end of file diff --git a/src/components/Dashboard/DashboardChat/DashboardChatArea/README.md b/src/components/Dashboard/DashboardChat/DashboardChatArea/README.md new file mode 100644 index 0000000..499aec7 --- /dev/null +++ b/src/components/Dashboard/DashboardChat/DashboardChatArea/README.md @@ -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` \ No newline at end of file diff --git a/src/components/Dashboard/DashboardChat/DashboardChatArea/dashboardChatAreaLogic.ts b/src/components/Dashboard/DashboardChat/DashboardChatArea/dashboardChatAreaLogic.ts new file mode 100644 index 0000000..fe7c95f --- /dev/null +++ b/src/components/Dashboard/DashboardChat/DashboardChatArea/dashboardChatAreaLogic.ts @@ -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(null); + const [workflowCompleted, setWorkflowCompleted] = useState(false); + const inputRef = useRef(null); + const messagesEndRef = useRef(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 + }; +}; \ No newline at end of file diff --git a/src/components/Dashboard/DashboardChat/DashboardChatArea/dashboardChatAreaTypes.ts b/src/components/Dashboard/DashboardChat/DashboardChatArea/dashboardChatAreaTypes.ts new file mode 100644 index 0000000..97bbe94 --- /dev/null +++ b/src/components/Dashboard/DashboardChat/DashboardChatArea/dashboardChatAreaTypes.ts @@ -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; + 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; +} + +export interface WorkflowStatusDisplayProps { + currentWorkflowId: string | null; + workflowStatus: WorkflowStatus | null; + workflowCompleted: boolean; + onStartNewWorkflow: () => void; +} \ No newline at end of file diff --git a/src/components/Dashboard/DashboardChat/DashboardChatArea/index.ts b/src/components/Dashboard/DashboardChat/DashboardChatArea/index.ts new file mode 100644 index 0000000..79a448b --- /dev/null +++ b/src/components/Dashboard/DashboardChat/DashboardChatArea/index.ts @@ -0,0 +1,2 @@ +export { default } from './DashboardChatArea'; +export type { DashboardChatAreaProps } from './dashboardChatAreaTypes'; \ No newline at end of file diff --git a/src/components/Dashboard/DashboardChat/DashboardChatHistory/DashboardChatHistoryItem.module.css b/src/components/Dashboard/DashboardChat/DashboardChatHistory/DashboardChatHistoryItem.module.css index 4ec8bcb..6368ded 100644 --- a/src/components/Dashboard/DashboardChat/DashboardChatHistory/DashboardChatHistoryItem.module.css +++ b/src/components/Dashboard/DashboardChat/DashboardChatHistory/DashboardChatHistoryItem.module.css @@ -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); } diff --git a/src/components/Dashboard/DashboardChat/DashboardChatHistory/DashboardChatHistoryItem.tsx b/src/components/Dashboard/DashboardChat/DashboardChatHistory/DashboardChatHistoryItem.tsx index 78194b5..37c7f22 100644 --- a/src/components/Dashboard/DashboardChat/DashboardChatHistory/DashboardChatHistoryItem.tsx +++ b/src/components/Dashboard/DashboardChat/DashboardChatHistory/DashboardChatHistoryItem.tsx @@ -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
{workflow.status.toUpperCase()} @@ -110,16 +132,11 @@ function DashboardChatHistoryItem({ workflow, onDelete, onResume }: DashboardCha
- First message:

{truncateMessage(messagePreview)}

- {workflow.name && ( -

- {workflow.name} -

- )} +
diff --git a/src/components/Dashboard/DashboardLog/DashboardLog.module.css b/src/components/Dashboard/DashboardLog/DashboardLog.module.css index cd8c775..51f2e0f 100644 --- a/src/components/Dashboard/DashboardLog/DashboardLog.module.css +++ b/src/components/Dashboard/DashboardLog/DashboardLog.module.css @@ -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); + } +} diff --git a/src/components/Dashboard/DashboardLog/DashboardLog.tsx b/src/components/Dashboard/DashboardLog/DashboardLog.tsx index d8b29c7..78ad301 100644 --- a/src/components/Dashboard/DashboardLog/DashboardLog.tsx +++ b/src/components/Dashboard/DashboardLog/DashboardLog.tsx @@ -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 = ({ isExpanded, workflowId }) => { - const { status: workflowStatus } = useWorkflowStatus(workflowId); +const DashboardLog: React.FC = ({ isExpanded, workflowId, workflowCompleted = false }) => { + const [logs, setLogs] = useState([]); + const [isPolling, setIsPolling] = useState(false); + const [logsError, setLogsError] = useState(null); + const intervalRef = useRef(null); + const consoleContainerRef = useRef(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(null); + const { request, isLoading: logsLoading } = useApiRequest(); - // 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 ( +
+ $ + No workflow selected +
+ ); + } + + if (logsLoading && logs.length === 0) { + return ( +
+ $ + Loading logs... +
+ ); + } + + if (logsError) { + return ( +
+ $ + Error loading logs: {logsError} +
+ ); + } + + if (logs.length === 0) { + const statusText = workflowCompleted + ? "No logs available for this workflow" + : "Workflow running... Waiting for logs..."; + + return ( +
+ $ + {statusText} + {isPolling && |} +
+ ); + } + + return ( +
+ {logs.map((log, index) => ( +
+ + {log.timestamp ? new Date(log.timestamp).toLocaleTimeString() : ''} + + [{log.level || 'INFO'}] + {log.message || log.content || JSON.stringify(log)} +
+ ))} +
+ ); }; return ( @@ -103,53 +200,9 @@ const DashboardLog: React.FC = ({ isExpanded, workflowId }) = animate={{ opacity: 1, height: "auto" }} transition={{ duration: 0.4, ease: "easeOut" }} > -
+
- {!workflowId ? ( -
- $ - Waiting for workflow to start... -
- ) : loading && logs.length === 0 ? ( -
- $ - Loading workflow logs... - _ -
- ) : error ? ( -
- $ - Error loading logs: {error} -
- ) : logs.length > 0 ? ( - logs.map((log, index) => ( -
- - [{formatTimestamp(log.timestamp || '')}] - - - [{(log.level || 'INFO').toUpperCase()}] - - - {log.message || 'No message'} - - {log.data && ( -
- {JSON.stringify(log.data, null, 2)} -
- )} -
- )) - ) : ( -
- $ - No logs available for this workflow -
- )} -
+ {renderLogContent()}
diff --git a/src/hooks/useWorkflows.ts b/src/hooks/useWorkflows.ts index 79c9d99..174a31a 100644 --- a/src/hooks/useWorkflows.ts +++ b/src/hooks/useWorkflows.ts @@ -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; -} - 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([]); - const { request, isLoading: loading, error } = useApiRequest(); +export function useWorkflowLogs(workflowId: string | null, logId?: string) { + const [logs, setLogs] = useState([]); + const { request, isLoading: loading, error } = useApiRequest(); - 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 }; } \ No newline at end of file diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx index d22827a..8d1c68f 100644 --- a/src/pages/Dashboard.tsx +++ b/src/pages/Dashboard.tsx @@ -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(null); const [isPromptAreaCollapsed, setIsPromptAreaCollapsed] = useState(false); const [currentWorkflowId, setCurrentWorkflowId] = useState(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 (
setSelectedPrompt(null)} onWorkflowIdChange={handleWorkflowIdChange} + onWorkflowCompletedChange={handleWorkflowCompletedChange} onWorkflowResume={handleWorkflowResume} />