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