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;
|
selectedPrompt?: Prompt | null;
|
||||||
onPromptUsed?: () => void;
|
onPromptUsed?: () => void;
|
||||||
onWorkflowIdChange?: (workflowId: string | null) => void;
|
onWorkflowIdChange?: (workflowId: string | null) => void;
|
||||||
|
onWorkflowCompletedChange?: (completed: boolean) => void;
|
||||||
onWorkflowResume?: (workflowId: string) => void;
|
onWorkflowResume?: (workflowId: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -23,6 +24,7 @@ const DashboardChat: React.FC<DashboardChatProps> = ({
|
||||||
selectedPrompt,
|
selectedPrompt,
|
||||||
onPromptUsed,
|
onPromptUsed,
|
||||||
onWorkflowIdChange,
|
onWorkflowIdChange,
|
||||||
|
onWorkflowCompletedChange,
|
||||||
onWorkflowResume
|
onWorkflowResume
|
||||||
}) => {
|
}) => {
|
||||||
const [activeTab, setActiveTab] = useState("Chat Area");
|
const [activeTab, setActiveTab] = useState("Chat Area");
|
||||||
|
|
@ -119,6 +121,7 @@ const DashboardChat: React.FC<DashboardChatProps> = ({
|
||||||
selectedPrompt={selectedPrompt}
|
selectedPrompt={selectedPrompt}
|
||||||
onPromptUsed={onPromptUsed}
|
onPromptUsed={onPromptUsed}
|
||||||
onWorkflowIdChange={onWorkflowIdChange}
|
onWorkflowIdChange={onWorkflowIdChange}
|
||||||
|
onWorkflowCompletedChange={onWorkflowCompletedChange}
|
||||||
resumeWorkflowId={resumeWorkflowId}
|
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;
|
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 {
|
.loading_message {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
background-color: #e3f2fd;
|
background-color: #e3f2fd;
|
||||||
|
|
|
||||||
|
|
@ -1,276 +1,87 @@
|
||||||
import React, { useState, useEffect, useRef } from "react";
|
import React, { useEffect } from "react";
|
||||||
import { motion } from "framer-motion";
|
import { DashboardChatAreaProps } from "./dashboardChatAreaTypes";
|
||||||
import { LuSendHorizontal } from "react-icons/lu";
|
import { useChatLogic } from "./dashboardChatAreaLogic";
|
||||||
import { Prompt } from "../../../../hooks/usePrompts";
|
import MessageList from "./DashboardChatAreaMessageList";
|
||||||
import { useWorkflowOperations, useWorkflowMessages, useWorkflowStatus } from "../../../../hooks/useWorkflows";
|
import ChatInput from "./DashboardChatAreaInput";
|
||||||
|
|
||||||
import styles from './DashboardChatArea.module.css';
|
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> = ({
|
const DashboardChatArea: React.FC<DashboardChatAreaProps> = ({
|
||||||
selectedPrompt,
|
selectedPrompt,
|
||||||
onPromptUsed,
|
onPromptUsed,
|
||||||
onWorkflowIdChange,
|
onWorkflowIdChange,
|
||||||
|
onWorkflowCompletedChange,
|
||||||
resumeWorkflowId
|
resumeWorkflowId
|
||||||
}) => {
|
}) => {
|
||||||
const [inputValue, setInputValue] = useState("");
|
const {
|
||||||
const [currentWorkflowId, setCurrentWorkflowId] = useState<string | null>(null);
|
// State
|
||||||
const [workflowCompleted, setWorkflowCompleted] = useState(false);
|
inputValue,
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
setInputValue,
|
||||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
currentWorkflowId,
|
||||||
const { startWorkflow, startingWorkflow, startError } = useWorkflowOperations();
|
workflowCompleted,
|
||||||
const { messages, loading: messagesLoading, error: messagesError, refetch: refetchMessages } = useWorkflowMessages(currentWorkflowId);
|
|
||||||
const { status: workflowStatus, refetch: refetchStatus } = useWorkflowStatus(currentWorkflowId);
|
// 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(() => {
|
useEffect(() => {
|
||||||
if (selectedPrompt) {
|
if (onWorkflowCompletedChange) {
|
||||||
setInputValue(selectedPrompt.content);
|
onWorkflowCompletedChange(workflowCompleted);
|
||||||
// Focus the input field
|
|
||||||
if (inputRef.current) {
|
|
||||||
inputRef.current.focus();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, [selectedPrompt]);
|
}, [workflowCompleted, onWorkflowCompletedChange]);
|
||||||
|
|
||||||
// Auto-scroll to bottom when new messages arrive
|
const placeholder = workflowCompleted ? "Continue the conversation..." : "Type your message...";
|
||||||
useEffect(() => {
|
|
||||||
if (messagesEndRef.current) {
|
|
||||||
messagesEndRef.current.scrollIntoView({ behavior: 'smooth' });
|
|
||||||
}
|
|
||||||
}, [messages]);
|
|
||||||
|
|
||||||
// Polling logic for fetching messages and status
|
|
||||||
useEffect(() => {
|
|
||||||
if (!currentWorkflowId || workflowCompleted) return;
|
|
||||||
|
|
||||||
const interval = setInterval(() => {
|
|
||||||
refetchMessages();
|
|
||||||
refetchStatus();
|
|
||||||
}, 1000); // Poll every second
|
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
|
||||||
}, [currentWorkflowId, workflowCompleted, refetchMessages, refetchStatus]);
|
|
||||||
|
|
||||||
// Check if workflow is completed based on status or messages
|
|
||||||
useEffect(() => {
|
|
||||||
if (workflowStatus && (
|
|
||||||
workflowStatus.status === 'completed' ||
|
|
||||||
workflowStatus.status === 'finished' ||
|
|
||||||
workflowStatus.status === 'done' ||
|
|
||||||
workflowStatus.status === 'stopped'
|
|
||||||
)) {
|
|
||||||
setWorkflowCompleted(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (messages.length > 0) {
|
|
||||||
const lastMessage = messages[messages.length - 1];
|
|
||||||
// Check if the last message indicates completion
|
|
||||||
if (lastMessage.role === 'assistant' &&
|
|
||||||
(lastMessage.content.toLowerCase().includes('completed') ||
|
|
||||||
lastMessage.content.toLowerCase().includes('finished') ||
|
|
||||||
lastMessage.content.toLowerCase().includes('done') ||
|
|
||||||
lastMessage.content.toLowerCase().includes('workflow completed'))) {
|
|
||||||
setWorkflowCompleted(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [messages, workflowStatus]);
|
|
||||||
|
|
||||||
const handleSend = async () => {
|
|
||||||
if (inputValue.trim()) {
|
|
||||||
console.log('Sending message:', inputValue);
|
|
||||||
|
|
||||||
try {
|
|
||||||
let result;
|
|
||||||
|
|
||||||
// If we have a completed workflow, send as follow-up using the existing workflow ID
|
|
||||||
if (workflowCompleted && currentWorkflowId) {
|
|
||||||
console.log('Sending follow-up message to workflow:', currentWorkflowId);
|
|
||||||
result = await startWorkflow({
|
|
||||||
prompt: inputValue,
|
|
||||||
listFileId: []
|
|
||||||
}, currentWorkflowId);
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
console.log('Follow-up message sent successfully');
|
|
||||||
// Reset workflow completion state to resume polling
|
|
||||||
setWorkflowCompleted(false);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Start a new workflow
|
|
||||||
console.log('Starting new workflow');
|
|
||||||
// Reset previous workflow state when starting a new one
|
|
||||||
setCurrentWorkflowId(null);
|
|
||||||
setWorkflowCompleted(false);
|
|
||||||
|
|
||||||
result = await startWorkflow({
|
|
||||||
prompt: inputValue,
|
|
||||||
listFileId: []
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.success && result.data) {
|
|
||||||
console.log('Workflow started successfully:', result.data);
|
|
||||||
// Set the workflow ID to start polling for messages
|
|
||||||
setCurrentWorkflowId(result.data.id);
|
|
||||||
setWorkflowCompleted(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
// Clear the input after successful send
|
|
||||||
setInputValue("");
|
|
||||||
// Call onPromptUsed if a prompt was used
|
|
||||||
if (selectedPrompt && onPromptUsed) {
|
|
||||||
onPromptUsed();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.error('Failed to send message:', result.error);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error sending message:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleKeyPress = (e: React.KeyboardEvent) => {
|
|
||||||
if (e.key === 'Enter' && !e.shiftKey) {
|
|
||||||
e.preventDefault();
|
|
||||||
handleSend();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const startNewWorkflow = () => {
|
|
||||||
setCurrentWorkflowId(null);
|
|
||||||
setWorkflowCompleted(false);
|
|
||||||
setInputValue("");
|
|
||||||
if (onWorkflowIdChange) {
|
|
||||||
onWorkflowIdChange(null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (currentWorkflowId && onWorkflowIdChange) {
|
|
||||||
onWorkflowIdChange(currentWorkflowId);
|
|
||||||
}
|
|
||||||
}, [currentWorkflowId, onWorkflowIdChange]);
|
|
||||||
|
|
||||||
// Handle workflow resumption
|
|
||||||
useEffect(() => {
|
|
||||||
if (resumeWorkflowId && resumeWorkflowId !== currentWorkflowId) {
|
|
||||||
console.log('Resuming workflow:', resumeWorkflowId);
|
|
||||||
setCurrentWorkflowId(resumeWorkflowId);
|
|
||||||
setWorkflowCompleted(false);
|
|
||||||
setInputValue("");
|
|
||||||
}
|
|
||||||
}, [resumeWorkflowId, currentWorkflowId]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.chat_area}>
|
<div className={styles.chat_area}>
|
||||||
<motion.div
|
<MessageList
|
||||||
className={styles.chat_messages}
|
messages={messages}
|
||||||
initial={{ opacity: 0 }}
|
currentWorkflowId={currentWorkflowId}
|
||||||
animate={{ opacity: 1 }}
|
workflowStatus={workflowStatus}
|
||||||
transition={{ delay: 0.2, duration: 0.3, ease: "easeOut" }}
|
workflowCompleted={workflowCompleted}
|
||||||
>
|
startingWorkflow={startingWorkflow}
|
||||||
<div className={styles.messages_container}>
|
startError={startError}
|
||||||
{startingWorkflow && (
|
messagesError={messagesError}
|
||||||
<div className={styles.loading_message}>
|
messagesLoading={messagesLoading}
|
||||||
<p>{workflowCompleted && currentWorkflowId ? 'Sending follow-up message...' : 'Sending message...'}</p>
|
onStartNewWorkflow={startNewWorkflow}
|
||||||
</div>
|
messagesEndRef={messagesEndRef}
|
||||||
)}
|
/>
|
||||||
{startError && (
|
<ChatInput
|
||||||
<div className={styles.error_message}>
|
inputValue={inputValue}
|
||||||
<p>Error: {startError}</p>
|
setInputValue={setInputValue}
|
||||||
</div>
|
onSend={handleSend}
|
||||||
)}
|
onKeyPress={handleKeyPress}
|
||||||
{messagesError && (
|
isDisabled={startingWorkflow}
|
||||||
<div className={styles.error_message}>
|
placeholder={placeholder}
|
||||||
<p>Error loading messages: {messagesError}</p>
|
inputRef={inputRef}
|
||||||
</div>
|
isWorkflowRunning={isWorkflowRunning}
|
||||||
)}
|
onStopWorkflow={handleStopWorkflow}
|
||||||
{currentWorkflowId && messagesLoading && messages.length === 0 && (
|
isStoppingWorkflow={isStoppingWorkflow}
|
||||||
<div className={styles.loading_message}>
|
/>
|
||||||
<p>Loading workflow messages...</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{messages.length > 0 ? (
|
|
||||||
messages.map((message, index) => (
|
|
||||||
<div
|
|
||||||
key={message.id || index}
|
|
||||||
className={`${styles.message} ${styles[`message_${message.role}`]}`}
|
|
||||||
>
|
|
||||||
<div className={styles.message_role}>
|
|
||||||
{message.role === 'user' ? 'You' :
|
|
||||||
message.role === 'assistant' ? 'Assistant' : 'System'}
|
|
||||||
</div>
|
|
||||||
<div className={styles.message_content}>
|
|
||||||
{message.content}
|
|
||||||
</div>
|
|
||||||
{message.timestamp && (
|
|
||||||
<div className={styles.message_timestamp}>
|
|
||||||
{new Date(message.timestamp).toLocaleTimeString()}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
) : !currentWorkflowId ? (
|
|
||||||
<p className={styles.placeholder_text}>Start a conversation by typing a message...</p>
|
|
||||||
) : null}
|
|
||||||
{currentWorkflowId && !workflowCompleted && (
|
|
||||||
<div className={styles.workflow_status}>
|
|
||||||
<p>
|
|
||||||
Workflow {currentWorkflowId.substring(0, 8)}... is {workflowStatus?.status || 'running'}
|
|
||||||
{workflowStatus?.currentRound && ` (Round ${workflowStatus.currentRound})`}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{workflowCompleted && (
|
|
||||||
<div className={styles.completion_message}>
|
|
||||||
<p>Workflow completed! You can continue the conversation or start a new workflow.</p>
|
|
||||||
<button
|
|
||||||
className={styles.new_workflow_button}
|
|
||||||
onClick={startNewWorkflow}
|
|
||||||
>
|
|
||||||
Start New Workflow
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div ref={messagesEndRef} />
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
<motion.div
|
|
||||||
className={styles.chat_input}
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ delay: 0.3, duration: 0.3, ease: "easeOut" }}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
ref={inputRef}
|
|
||||||
type="text"
|
|
||||||
value={inputValue}
|
|
||||||
onChange={(e) => setInputValue(e.target.value)}
|
|
||||||
onKeyPress={handleKeyPress}
|
|
||||||
placeholder={workflowCompleted ? "Continue the conversation..." : "Type your message..."}
|
|
||||||
className={styles.message_input}
|
|
||||||
disabled={startingWorkflow}
|
|
||||||
/>
|
|
||||||
<motion.button
|
|
||||||
className={styles.send_button}
|
|
||||||
onClick={handleSend}
|
|
||||||
disabled={startingWorkflow || !inputValue.trim()}
|
|
||||||
whileTap={{ scale: 0.95 }}
|
|
||||||
transition={{ duration: 0.2, ease: "easeOut" }}
|
|
||||||
>
|
|
||||||
<LuSendHorizontal className={styles.send_button_icon}/>
|
|
||||||
</motion.button>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
</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;
|
font-weight: 600;
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
background-color: rgba(0, 0, 0, 0.05);
|
background-color: #f4f3f5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.workflowRound {
|
.workflowRound {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #666;
|
color: #888098;
|
||||||
background-color: #f5f5f5;
|
background-color: #f4f3f5;
|
||||||
padding: 2px 6px;
|
padding: 2px 6px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
@ -79,24 +79,15 @@
|
||||||
.messagePreview {
|
.messagePreview {
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
background-color: #f8f9fa;
|
background-color: #f4f3f5;
|
||||||
border-radius: 6px;
|
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 {
|
.previewText {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: #444;
|
color: #888098;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
|
|
@ -137,22 +128,22 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.resumeButton {
|
.resumeButton {
|
||||||
background-color: #4CAF50;
|
background-color: #3a8088;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.resumeButton:hover:not(:disabled) {
|
.resumeButton:hover:not(:disabled) {
|
||||||
background-color: #45a049;
|
background-color: #34737b;
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.deleteButton {
|
.deleteButton {
|
||||||
background-color: #f44336;
|
background-color: #d85b65;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.deleteButton:hover:not(:disabled) {
|
.deleteButton:hover:not(:disabled) {
|
||||||
background-color: #da190b;
|
background-color: #c3525b;
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -53,18 +53,37 @@ function DashboardChatHistoryItem({ workflow, onDelete, onResume }: DashboardCha
|
||||||
case 'completed':
|
case 'completed':
|
||||||
case 'finished':
|
case 'finished':
|
||||||
case 'done':
|
case 'done':
|
||||||
return '#4CAF50';
|
return '#3a8088';
|
||||||
case 'running':
|
case 'running':
|
||||||
case 'processing':
|
case 'processing':
|
||||||
return '#2196F3';
|
return '#888098';
|
||||||
case 'error':
|
case 'error':
|
||||||
case 'failed':
|
case 'failed':
|
||||||
return '#F44336';
|
return '#d85d67';
|
||||||
case 'stopped':
|
case 'stopped':
|
||||||
case 'cancelled':
|
case 'cancelled':
|
||||||
return '#FF9800';
|
return '#d85d67';
|
||||||
default:
|
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}>
|
<div className={styles.workflowMeta}>
|
||||||
<span
|
<span
|
||||||
className={styles.workflowStatus}
|
className={styles.workflowStatus}
|
||||||
style={{ color: getStatusColor(workflow.status) }}
|
style={{
|
||||||
|
color: getStatusColor(workflow.status),
|
||||||
|
backgroundColor: getStatusBackgroundColor(workflow.status)
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{workflow.status.toUpperCase()}
|
{workflow.status.toUpperCase()}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -110,16 +132,11 @@ function DashboardChatHistoryItem({ workflow, onDelete, onResume }: DashboardCha
|
||||||
|
|
||||||
<div className={styles.workflowDescription}>
|
<div className={styles.workflowDescription}>
|
||||||
<div className={styles.messagePreview}>
|
<div className={styles.messagePreview}>
|
||||||
<span className={styles.previewLabel}>First message:</span>
|
|
||||||
<p className={styles.previewText}>
|
<p className={styles.previewText}>
|
||||||
{truncateMessage(messagePreview)}
|
{truncateMessage(messagePreview)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{workflow.name && (
|
|
||||||
<p className={styles.workflowName}>
|
|
||||||
{workflow.name}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -87,21 +87,23 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.log_entries {
|
.log_entries {
|
||||||
background-color: #f9f9f9;
|
display: flex;
|
||||||
border-radius: 15px;
|
flex-direction: column;
|
||||||
padding: 15px;
|
gap: 4px;
|
||||||
height: 100%;
|
scroll-behavior: smooth;
|
||||||
overflow-y: auto;
|
}
|
||||||
font-family: 'Courier New', monospace;
|
|
||||||
|
.log_entries::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.log_entry {
|
.log_entry {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 15px;
|
align-items: flex-start;
|
||||||
padding: 8px 0;
|
gap: 8px;
|
||||||
border-bottom: 1px solid #e0e0e0;
|
padding: 4px 0;
|
||||||
align-items: center;
|
border-bottom: 1px solid rgba(0, 255, 0, 0.1);
|
||||||
font-size: 12px;
|
animation: fadeIn 0.3s ease-in;
|
||||||
}
|
}
|
||||||
|
|
||||||
.log_entry:last-child {
|
.log_entry:last-child {
|
||||||
|
|
@ -109,9 +111,25 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.log_timestamp {
|
.log_timestamp {
|
||||||
color: #666;
|
color: #888;
|
||||||
min-width: 140px;
|
font-size: 11px;
|
||||||
font-weight: 500;
|
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 {
|
.log_level_info {
|
||||||
|
|
@ -147,18 +165,15 @@
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.log_message {
|
|
||||||
flex: 1;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Hacker-style console styles */
|
/* Hacker-style console styles */
|
||||||
.console_container {
|
.console_container {
|
||||||
background-color: #0a0a0a;
|
background-color: #0a0a0a;
|
||||||
border-radius: 8px;
|
border-radius: 15px;
|
||||||
|
height: 100%;
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 200px;
|
min-height: 0;
|
||||||
|
max-height: 100%;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
font-family: 'Courier New', 'Monaco', 'Menlo', monospace;
|
font-family: 'Courier New', 'Monaco', 'Menlo', monospace;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
|
@ -305,3 +320,22 @@
|
||||||
opacity: 0;
|
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 { motion } from "framer-motion";
|
||||||
import { useWorkflowLogs, useWorkflowStatus } from "../../../hooks/useWorkflows";
|
|
||||||
|
|
||||||
import styles from './DashboardLog.module.css';
|
import styles from './DashboardLog.module.css';
|
||||||
|
import { useApiRequest } from '../../../hooks/useApi';
|
||||||
|
|
||||||
interface DashboardLogProps {
|
interface DashboardLogProps {
|
||||||
isExpanded: boolean;
|
isExpanded: boolean;
|
||||||
workflowId: string | null;
|
workflowId: string | null;
|
||||||
|
workflowCompleted?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DashboardLog: React.FC<DashboardLogProps> = ({ isExpanded, workflowId }) => {
|
const DashboardLog: React.FC<DashboardLogProps> = ({ isExpanded, workflowId, workflowCompleted = false }) => {
|
||||||
const { status: workflowStatus } = useWorkflowStatus(workflowId);
|
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 { request, isLoading: logsLoading } = useApiRequest<null, any[]>();
|
||||||
const workflowCompleted = workflowStatus && (
|
|
||||||
workflowStatus.status === 'completed' ||
|
|
||||||
workflowStatus.status === 'finished' ||
|
|
||||||
workflowStatus.status === 'done' ||
|
|
||||||
workflowStatus.status === 'stopped'
|
|
||||||
);
|
|
||||||
|
|
||||||
const { logs, loading, error } = useWorkflowLogs(workflowId, undefined, !!workflowId, !!workflowCompleted);
|
|
||||||
const logsEndRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
// Auto-scroll to bottom when new logs arrive
|
// Function to fetch logs directly
|
||||||
useEffect(() => {
|
const fetchLogs = async (workflowIdToFetch: string) => {
|
||||||
if (logsEndRef.current) {
|
|
||||||
logsEndRef.current.scrollIntoView({ behavior: 'smooth' });
|
|
||||||
}
|
|
||||||
}, [logs]);
|
|
||||||
|
|
||||||
const formatTimestamp = (timestamp: string) => {
|
|
||||||
if (!timestamp) return '00:00:00';
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return new Date(timestamp).toLocaleTimeString('en-US', {
|
console.log('Fetching logs for workflow:', workflowIdToFetch);
|
||||||
hour12: false,
|
const data = await request({
|
||||||
hour: '2-digit',
|
url: `/api/workflows/${workflowIdToFetch}/logs`,
|
||||||
minute: '2-digit',
|
method: 'get'
|
||||||
second: '2-digit'
|
|
||||||
});
|
});
|
||||||
} 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) => {
|
// Auto-scroll to bottom when new logs arrive during polling
|
||||||
if (!level) return '#00ff00'; // Default green if level is undefined
|
useEffect(() => {
|
||||||
|
if (isPolling && consoleContainerRef.current && logs.length > 0) {
|
||||||
switch (level.toLowerCase()) {
|
consoleContainerRef.current.scrollTop = consoleContainerRef.current.scrollHeight;
|
||||||
case 'error':
|
|
||||||
return '#ff4444';
|
|
||||||
case 'warn':
|
|
||||||
case 'warning':
|
|
||||||
return '#ffaa00';
|
|
||||||
case 'info':
|
|
||||||
return '#00aaff';
|
|
||||||
case 'debug':
|
|
||||||
return '#888888';
|
|
||||||
default:
|
|
||||||
return '#00ff00';
|
|
||||||
}
|
}
|
||||||
|
}, [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 (
|
return (
|
||||||
|
|
@ -103,53 +200,9 @@ const DashboardLog: React.FC<DashboardLogProps> = ({ isExpanded, workflowId }) =
|
||||||
animate={{ opacity: 1, height: "auto" }}
|
animate={{ opacity: 1, height: "auto" }}
|
||||||
transition={{ duration: 0.4, ease: "easeOut" }}
|
transition={{ duration: 0.4, ease: "easeOut" }}
|
||||||
>
|
>
|
||||||
<div className={styles.console_container}>
|
<div className={styles.console_container} ref={consoleContainerRef}>
|
||||||
<div className={styles.console_content}>
|
<div className={styles.console_content}>
|
||||||
{!workflowId ? (
|
{renderLogContent()}
|
||||||
<div className={styles.console_placeholder}>
|
|
||||||
<span className={styles.console_prompt}>$</span>
|
|
||||||
<span className={styles.console_text}>Waiting for workflow to start...</span>
|
|
||||||
</div>
|
|
||||||
) : loading && logs.length === 0 ? (
|
|
||||||
<div className={styles.console_loading}>
|
|
||||||
<span className={styles.console_prompt}>$</span>
|
|
||||||
<span className={styles.console_text}>Loading workflow logs...</span>
|
|
||||||
<span className={styles.console_cursor}>_</span>
|
|
||||||
</div>
|
|
||||||
) : error ? (
|
|
||||||
<div className={styles.console_error}>
|
|
||||||
<span className={styles.console_prompt}>$</span>
|
|
||||||
<span className={styles.console_text}>Error loading logs: {error}</span>
|
|
||||||
</div>
|
|
||||||
) : logs.length > 0 ? (
|
|
||||||
logs.map((log, index) => (
|
|
||||||
<div key={log.id || index} className={styles.console_line}>
|
|
||||||
<span className={styles.console_timestamp}>
|
|
||||||
[{formatTimestamp(log.timestamp || '')}]
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
className={styles.console_level}
|
|
||||||
style={{ color: getLevelColor(log.level || 'info') }}
|
|
||||||
>
|
|
||||||
[{(log.level || 'INFO').toUpperCase()}]
|
|
||||||
</span>
|
|
||||||
<span className={styles.console_message}>
|
|
||||||
{log.message || 'No message'}
|
|
||||||
</span>
|
|
||||||
{log.data && (
|
|
||||||
<div className={styles.console_data}>
|
|
||||||
{JSON.stringify(log.data, null, 2)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<div className={styles.console_empty}>
|
|
||||||
<span className={styles.console_prompt}>$</span>
|
|
||||||
<span className={styles.console_text}>No logs available for this workflow</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div ref={logsEndRef} />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||||
import { useApiRequest } from './useApi';
|
import { useApiRequest } from './useApi';
|
||||||
|
|
||||||
// Workflow interfaces
|
// Workflow interfaces
|
||||||
|
|
@ -23,14 +23,6 @@ export interface WorkflowMessage {
|
||||||
fileIds?: number[];
|
fileIds?: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WorkflowLog {
|
|
||||||
id: string;
|
|
||||||
level: string;
|
|
||||||
message: string;
|
|
||||||
timestamp: string;
|
|
||||||
data?: Record<string, any>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface StartWorkflowRequest {
|
export interface StartWorkflowRequest {
|
||||||
prompt: string;
|
prompt: string;
|
||||||
listFileId: number[];
|
listFileId: number[];
|
||||||
|
|
@ -214,11 +206,11 @@ export function useWorkflowMessages(workflowId: string | null, messageId?: strin
|
||||||
}
|
}
|
||||||
|
|
||||||
// Workflow logs hook
|
// Workflow logs hook
|
||||||
export function useWorkflowLogs(workflowId: string | null, logId?: string, enablePolling: boolean = false, workflowCompleted: boolean = false) {
|
export function useWorkflowLogs(workflowId: string | null, logId?: string) {
|
||||||
const [logs, setLogs] = useState<WorkflowLog[]>([]);
|
const [logs, setLogs] = useState<any[]>([]);
|
||||||
const { request, isLoading: loading, error } = useApiRequest<null, WorkflowLog[]>();
|
const { request, isLoading: loading, error } = useApiRequest<null, any[]>();
|
||||||
|
|
||||||
const fetchLogs = useCallback(async () => {
|
const fetchLogs = async () => {
|
||||||
if (!workflowId) return;
|
if (!workflowId) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -232,29 +224,11 @@ export function useWorkflowLogs(workflowId: string | null, logId?: string, enabl
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Error is already handled by useApiRequest
|
// Error is already handled by useApiRequest
|
||||||
}
|
}
|
||||||
}, [workflowId, logId, request]);
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchLogs();
|
fetchLogs();
|
||||||
}, [fetchLogs]);
|
}, [workflowId, logId]);
|
||||||
|
|
||||||
// Polling effect for real-time log updates - poll every second until workflow is completed
|
|
||||||
useEffect(() => {
|
|
||||||
if (!workflowId || !enablePolling || workflowCompleted) return;
|
|
||||||
|
|
||||||
const interval = setInterval(() => {
|
|
||||||
fetchLogs();
|
|
||||||
}, 1000); // Poll every second for logs
|
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
|
||||||
}, [workflowId, enablePolling, workflowCompleted, fetchLogs]);
|
|
||||||
|
|
||||||
// Clear logs when workflowId changes
|
|
||||||
useEffect(() => {
|
|
||||||
if (!workflowId) {
|
|
||||||
setLogs([]);
|
|
||||||
}
|
|
||||||
}, [workflowId]);
|
|
||||||
|
|
||||||
return { logs, loading, error, refetch: fetchLogs };
|
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 DashboardPrompt from '../components/Dashboard/DashboardPrompt/DashboardPrompt';
|
||||||
import DashboardChat from '../components/Dashboard/DashboardChat/DashboardChat';
|
import DashboardChat from '../components/Dashboard/DashboardChat/DashboardChat';
|
||||||
import DashboardLog from '../components/Dashboard/DashboardLog/DashboardLog';
|
import DashboardLog from '../components/Dashboard/DashboardLog/DashboardLog';
|
||||||
|
|
@ -10,6 +10,7 @@ function Dashboard () {
|
||||||
const [selectedPrompt, setSelectedPrompt] = useState<Prompt | null>(null);
|
const [selectedPrompt, setSelectedPrompt] = useState<Prompt | null>(null);
|
||||||
const [isPromptAreaCollapsed, setIsPromptAreaCollapsed] = useState(false);
|
const [isPromptAreaCollapsed, setIsPromptAreaCollapsed] = useState(false);
|
||||||
const [currentWorkflowId, setCurrentWorkflowId] = useState<string | null>(null);
|
const [currentWorkflowId, setCurrentWorkflowId] = useState<string | null>(null);
|
||||||
|
const [workflowCompleted, setWorkflowCompleted] = useState(false);
|
||||||
|
|
||||||
const handleChatToggleExpand = () => {
|
const handleChatToggleExpand = () => {
|
||||||
setIsChatExpanded(!isChatExpanded);
|
setIsChatExpanded(!isChatExpanded);
|
||||||
|
|
@ -22,13 +23,22 @@ function Dashboard () {
|
||||||
|
|
||||||
const handleWorkflowIdChange = useCallback((workflowId: string | null) => {
|
const handleWorkflowIdChange = useCallback((workflowId: string | null) => {
|
||||||
setCurrentWorkflowId(workflowId);
|
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) => {
|
const handleWorkflowResume = useCallback((workflowId: string) => {
|
||||||
// Set the workflow ID to resume it
|
// Set the workflow ID to resume it
|
||||||
setCurrentWorkflowId(workflowId);
|
setCurrentWorkflowId(workflowId);
|
||||||
|
// Reset completion status when resuming
|
||||||
|
setWorkflowCompleted(false);
|
||||||
// Switch to Chat Area tab to show the resumed workflow
|
// Switch to Chat Area tab to show the resumed workflow
|
||||||
console.log('Resuming workflow:', workflowId);
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Determine CSS classes based on states
|
// Determine CSS classes based on states
|
||||||
|
|
@ -51,13 +61,27 @@ function Dashboard () {
|
||||||
return styles.logArea40vh;
|
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 (
|
return (
|
||||||
<div className={styles.dashboardContainer}>
|
<div className={styles.dashboardContainer}>
|
||||||
<div
|
<div
|
||||||
className={getPromptClass()}
|
className={getPromptClass()}
|
||||||
style={{
|
style={promptStyle}
|
||||||
marginBottom: !isPromptAreaCollapsed ? "40px" : "0"
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<DashboardPrompt
|
<DashboardPrompt
|
||||||
onPromptRun={handlePromptRun}
|
onPromptRun={handlePromptRun}
|
||||||
|
|
@ -68,11 +92,7 @@ function Dashboard () {
|
||||||
<div className={`${styles.chatLogContainer} ${isChatExpanded ? styles.expanded : ''}`}>
|
<div className={`${styles.chatLogContainer} ${isChatExpanded ? styles.expanded : ''}`}>
|
||||||
<div
|
<div
|
||||||
className={getChatClass()}
|
className={getChatClass()}
|
||||||
style={{
|
style={chatStyle}
|
||||||
width: isChatExpanded ? "100%" : "calc(50% - 10px)",
|
|
||||||
flex: isChatExpanded ? "none" : "1",
|
|
||||||
marginBottom: isChatExpanded ? "40px" : "0"
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<DashboardChat
|
<DashboardChat
|
||||||
isExpanded={isChatExpanded}
|
isExpanded={isChatExpanded}
|
||||||
|
|
@ -80,19 +100,18 @@ function Dashboard () {
|
||||||
selectedPrompt={selectedPrompt}
|
selectedPrompt={selectedPrompt}
|
||||||
onPromptUsed={() => setSelectedPrompt(null)}
|
onPromptUsed={() => setSelectedPrompt(null)}
|
||||||
onWorkflowIdChange={handleWorkflowIdChange}
|
onWorkflowIdChange={handleWorkflowIdChange}
|
||||||
|
onWorkflowCompletedChange={handleWorkflowCompletedChange}
|
||||||
onWorkflowResume={handleWorkflowResume}
|
onWorkflowResume={handleWorkflowResume}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={getLogClass()}
|
className={getLogClass()}
|
||||||
style={{
|
style={logStyle}
|
||||||
width: isChatExpanded ? "100%" : "calc(50% - 10px)",
|
|
||||||
flex: isChatExpanded ? "none" : "1"
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<DashboardLog
|
<DashboardLog
|
||||||
isExpanded={isChatExpanded}
|
isExpanded={isChatExpanded}
|
||||||
workflowId={currentWorkflowId}
|
workflowId={currentWorkflowId}
|
||||||
|
workflowCompleted={workflowCompleted}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue