added functionality for workflow selection across platform
This commit is contained in:
parent
22d582f72b
commit
e616541ee1
9 changed files with 492 additions and 112 deletions
|
|
@ -1,8 +1,7 @@
|
|||
import React, { useState } from "react";
|
||||
import React from "react";
|
||||
import { Prompt } from "../../../hooks/usePrompts";
|
||||
import { useLanguage } from '../../../contexts/LanguageContext';
|
||||
|
||||
import DashboardChatArea from './DashboardChatArea';
|
||||
import DashboardChatArea from './DashboardChatArea.tsx';
|
||||
|
||||
import styles from './DashboardChatAreaStyles/DashboardChat.module.css';
|
||||
|
||||
|
|
@ -13,7 +12,7 @@ interface DashboardChatProps {
|
|||
onPromptUsed?: () => void;
|
||||
onWorkflowIdChange?: (workflowId: string | null) => void;
|
||||
onWorkflowCompletedChange?: (completed: boolean) => void;
|
||||
onWorkflowResume?: (workflowId: string) => void;
|
||||
currentWorkflowId?: string | null;
|
||||
}
|
||||
|
||||
const DashboardChat: React.FC<DashboardChatProps> = ({
|
||||
|
|
@ -21,23 +20,11 @@ const DashboardChat: React.FC<DashboardChatProps> = ({
|
|||
onPromptUsed,
|
||||
onWorkflowIdChange,
|
||||
onWorkflowCompletedChange,
|
||||
onWorkflowResume
|
||||
currentWorkflowId
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
const [resumeWorkflowId, setResumeWorkflowId] = useState<string | null>(null);
|
||||
|
||||
const handleWorkflowResume = (workflowId: string) => {
|
||||
// Switch to Chat Area tab first
|
||||
setResumeWorkflowId(workflowId);
|
||||
// Then call the parent's resume handler
|
||||
if (onWorkflowResume) {
|
||||
onWorkflowResume(workflowId);
|
||||
}
|
||||
// Clear the resume ID after a short delay to allow processing
|
||||
setTimeout(() => {
|
||||
setResumeWorkflowId(null);
|
||||
}, 100);
|
||||
};
|
||||
// Pass the current workflow ID directly to the chat area
|
||||
// This handles both dropdown selection and workflow resume scenarios
|
||||
const effectiveWorkflowId = currentWorkflowId;
|
||||
|
||||
return (
|
||||
<div className={styles.dashboard_chat}>
|
||||
|
|
@ -46,7 +33,7 @@ const DashboardChat: React.FC<DashboardChatProps> = ({
|
|||
onPromptUsed={onPromptUsed}
|
||||
onWorkflowIdChange={onWorkflowIdChange}
|
||||
onWorkflowCompletedChange={onWorkflowCompletedChange}
|
||||
resumeWorkflowId={resumeWorkflowId}
|
||||
resumeWorkflowId={effectiveWorkflowId}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useState } from "react";
|
||||
import React, { useState, useRef } from "react";
|
||||
import MessageList from "./DashboardChatAreaMessageList";
|
||||
import FilePreview from "./DashboardChatAreaFilePreview";
|
||||
import InputArea from "./DashboardChatAreaInput";
|
||||
|
|
@ -20,14 +20,23 @@ const DashboardChatArea: React.FC<DashboardChatAreaProps> = ({
|
|||
const [selectedFile, setSelectedFile] = useState<any>(null);
|
||||
const [attachedFiles, setAttachedFiles] = useState<any[]>([]);
|
||||
|
||||
// Track the last resumeWorkflowId to prevent feedback loops
|
||||
const lastResumeWorkflowIdRef = useRef<string | null>(resumeWorkflowId);
|
||||
|
||||
// Centralized workflow management
|
||||
const [workflowState, workflowActions] = useWorkflowManager(resumeWorkflowId);
|
||||
|
||||
// Notify parent when workflow ID changes
|
||||
// Only notify parent when a NEW workflow is created internally (not when selecting existing ones)
|
||||
// For workflow selection via dropdown, the Dashboard already manages the currentWorkflowId
|
||||
React.useEffect(() => {
|
||||
if (onWorkflowIdChange && workflowState.currentWorkflowId !== resumeWorkflowId) {
|
||||
// Only notify when a workflow is created from scratch (currentWorkflowId exists but resumeWorkflowId was null)
|
||||
if (onWorkflowIdChange && workflowState.currentWorkflowId && !resumeWorkflowId && !lastResumeWorkflowIdRef.current) {
|
||||
console.log(`🔔 Notifying parent of NEW workflow created: ${workflowState.currentWorkflowId}`);
|
||||
onWorkflowIdChange(workflowState.currentWorkflowId);
|
||||
}
|
||||
|
||||
// Update the ref to track the current resumeWorkflowId
|
||||
lastResumeWorkflowIdRef.current = resumeWorkflowId;
|
||||
}, [workflowState.currentWorkflowId, onWorkflowIdChange, resumeWorkflowId]);
|
||||
|
||||
// Notify parent when workflow is completed
|
||||
|
|
@ -38,13 +47,7 @@ const DashboardChatArea: React.FC<DashboardChatAreaProps> = ({
|
|||
}
|
||||
}, [workflowState.workflow?.status, onWorkflowCompletedChange]);
|
||||
|
||||
// Auto-load workflow when resumeWorkflowId changes externally
|
||||
React.useEffect(() => {
|
||||
if (resumeWorkflowId && resumeWorkflowId !== workflowState.currentWorkflowId) {
|
||||
console.log(`🔄 Loading workflow from external prop: ${resumeWorkflowId}`);
|
||||
workflowActions.loadWorkflow(resumeWorkflowId);
|
||||
}
|
||||
}, [resumeWorkflowId, workflowState.currentWorkflowId, workflowActions]);
|
||||
// Note: useWorkflowManager handles resumeWorkflowId changes automatically
|
||||
|
||||
// No resizing functionality needed
|
||||
|
||||
|
|
|
|||
|
|
@ -39,7 +39,9 @@ const InputArea: React.FC<InputAreaProps> = ({
|
|||
const [isSending, setIsSending] = useState(false);
|
||||
const [sendError, setSendError] = useState<string | null>(null);
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
const [isDragOver, setIsDragOver] = useState(false);
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const dropZoneRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Always use external attached files from parent component
|
||||
const currentAttachedFiles = externalAttachedFiles;
|
||||
|
|
@ -119,6 +121,25 @@ const InputArea: React.FC<InputAreaProps> = ({
|
|||
}
|
||||
};
|
||||
|
||||
const handleStop = async () => {
|
||||
setIsSending(true);
|
||||
setSendError(null);
|
||||
|
||||
try {
|
||||
console.log('🛑 Stopping workflow...');
|
||||
const success = await workflowActions.stopWorkflow();
|
||||
|
||||
if (!success) {
|
||||
setSendError('Failed to stop workflow. Please try again.');
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Failed to stop workflow:', error);
|
||||
setSendError(error.message || 'Failed to stop workflow. Please try again.');
|
||||
} finally {
|
||||
setIsSending(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyPress = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
|
|
@ -133,10 +154,73 @@ const InputArea: React.FC<InputAreaProps> = ({
|
|||
}
|
||||
};
|
||||
|
||||
// Drag and drop handlers
|
||||
const handleDragEnter = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (e.dataTransfer.types.includes('Files')) {
|
||||
setIsDragOver(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDragLeave = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
// Only hide drag over if we're leaving the drop zone entirely
|
||||
if (dropZoneRef.current && !dropZoneRef.current.contains(e.relatedTarget as Node)) {
|
||||
setIsDragOver(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDragOver = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
// Set the drop effect to copy
|
||||
e.dataTransfer.dropEffect = 'copy';
|
||||
};
|
||||
|
||||
const handleDrop = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsDragOver(false);
|
||||
|
||||
if (shouldShowStopButton) {
|
||||
// Don't allow file drops when workflow is active
|
||||
return;
|
||||
}
|
||||
|
||||
const files = Array.from(e.dataTransfer.files);
|
||||
if (files.length > 0) {
|
||||
// Convert File objects to AttachedFile format
|
||||
const attachedFiles: AttachedFile[] = files.map((file, index) => ({
|
||||
id: Date.now() + index, // Simple ID generation
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
type: file.type,
|
||||
fileData: file,
|
||||
objectUrl: URL.createObjectURL(file)
|
||||
}));
|
||||
|
||||
// Add to existing attached files
|
||||
const updatedFiles = [...currentAttachedFiles, ...attachedFiles];
|
||||
if (onAttachedFilesChange) {
|
||||
onAttachedFilesChange(updatedFiles);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
// Check if workflow is in an active state where we should show stop button
|
||||
const isWorkflowActive = workflowState.workflow &&
|
||||
['running', 'processing', 'started'].includes(workflowState.workflow.status);
|
||||
|
||||
|
||||
|
||||
// Use polling as an indicator that workflow might be active
|
||||
const shouldShowStopButton = isWorkflowActive || (workflowState.isPolling && workflowState.currentWorkflowId);
|
||||
|
||||
|
||||
|
||||
// Determine if label should be in focused/moved state
|
||||
const shouldLabelBeFocused = isFocused || inputValue.trim().length > 0;
|
||||
|
|
@ -147,7 +231,14 @@ const InputArea: React.FC<InputAreaProps> = ({
|
|||
: t('chat.input.enter_message');
|
||||
|
||||
return (
|
||||
<div className={styles.input_area_container}>
|
||||
<div
|
||||
ref={dropZoneRef}
|
||||
className={`${styles.input_area_container} ${isDragOver ? styles.drag_over : ''}`}
|
||||
onDragEnter={handleDragEnter}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDragOver={handleDragOver}
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
|
||||
{/* Error messages */}
|
||||
{(sendError || workflowState.error) && (
|
||||
|
|
@ -156,6 +247,23 @@ const InputArea: React.FC<InputAreaProps> = ({
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* Drag and drop overlay */}
|
||||
{isDragOver && (
|
||||
<div className={`${styles.drag_overlay} ${shouldShowStopButton ? styles.disabled : ''}`}>
|
||||
<div className={styles.drag_overlay_content}>
|
||||
<div className={styles.drag_icon}>
|
||||
{shouldShowStopButton ? '🚫' : '📁'}
|
||||
</div>
|
||||
<div className={styles.drag_text}>
|
||||
{shouldShowStopButton
|
||||
? t('chat.input.drop_disabled')
|
||||
: t('chat.input.drop_files_here')
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Show attached files count */}
|
||||
{currentAttachedFiles.length > 0 && (
|
||||
<div className={styles.attached_files_count}>
|
||||
|
|
@ -175,14 +283,13 @@ const InputArea: React.FC<InputAreaProps> = ({
|
|||
value={inputValue}
|
||||
onChange={(e) => {
|
||||
setInputValue(e.target.value);
|
||||
// Trigger resize on next frame to ensure DOM is updated
|
||||
setTimeout(adjustTextareaHeight, 0);
|
||||
}}
|
||||
onKeyPress={handleKeyPress}
|
||||
onFocus={() => setIsFocused(true)}
|
||||
onBlur={() => setIsFocused(false)}
|
||||
placeholder=""
|
||||
disabled={isSending || isWorkflowActive}
|
||||
disabled={isSending || shouldShowStopButton}
|
||||
className={styles.message_textarea}
|
||||
rows={4}
|
||||
/>
|
||||
|
|
@ -191,27 +298,40 @@ const InputArea: React.FC<InputAreaProps> = ({
|
|||
<div className={styles.input_actions_row}>
|
||||
<button
|
||||
onClick={() => setShowFilePopup(true)}
|
||||
disabled={isSending || isWorkflowActive}
|
||||
className={sharedStyles.button_secondary}
|
||||
disabled={isSending || shouldShowStopButton}
|
||||
className={`${sharedStyles.button_secondary} ${
|
||||
(isSending || shouldShowStopButton) ? styles.disabled : ''
|
||||
}`}
|
||||
>
|
||||
{t('chat.input.attach_files')}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleSend}
|
||||
disabled={!inputValue.trim() || isSending || isWorkflowActive}
|
||||
className={`${sharedStyles.button_primary} ${
|
||||
(!inputValue.trim() || isSending || isWorkflowActive)
|
||||
? styles.disabled
|
||||
: styles.enabled
|
||||
}`}
|
||||
>
|
||||
{isSending ? t('chat.input.sending') :
|
||||
isWorkflowActive ? t('chat.input.processing') :
|
||||
workflowState.currentWorkflowId ? t('chat.input.continue') : t('chat.input.send')}
|
||||
</button>
|
||||
{shouldShowStopButton ? (
|
||||
<button
|
||||
onClick={handleStop}
|
||||
disabled={isSending}
|
||||
className={`${sharedStyles.button_primary} ${
|
||||
isSending ? styles.disabled : styles.enabled
|
||||
}`}
|
||||
>
|
||||
{isSending ? t('chat.input.stopping') : t('chat.input.stop')}
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={handleSend}
|
||||
disabled={!inputValue.trim() || isSending}
|
||||
className={`${sharedStyles.button_primary} ${
|
||||
(!inputValue.trim() || isSending)
|
||||
? styles.disabled
|
||||
: styles.enabled
|
||||
}`}
|
||||
>
|
||||
{isSending ? t('chat.input.sending') :
|
||||
workflowState.currentWorkflowId ? t('chat.input.continue') : t('chat.input.send')}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{workflowState.currentWorkflowId && !isWorkflowActive && (
|
||||
{workflowState.currentWorkflowId && !shouldShowStopButton && (
|
||||
<button
|
||||
onClick={() => workflowActions.clearWorkflow()}
|
||||
className={styles.new_chat_button}
|
||||
|
|
|
|||
|
|
@ -135,3 +135,55 @@
|
|||
font-size: 12px;
|
||||
color: var(--color-gray);
|
||||
}
|
||||
|
||||
/* Drag and Drop Styles */
|
||||
.input_area_container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.drag_over {
|
||||
border: 2px dashed var(--color-primary);
|
||||
background-color: rgba(var(--color-primary-rgb), 0.05);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.drag_overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(var(--color-primary-rgb), 0.1);
|
||||
backdrop-filter: blur(2px);
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10;
|
||||
border: 2px dashed var(--color-primary);
|
||||
}
|
||||
|
||||
.drag_overlay_content {
|
||||
text-align: center;
|
||||
color: var(--color-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.drag_icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.drag_text {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.drag_overlay.disabled {
|
||||
background-color: rgba(255, 0, 0, 0.1);
|
||||
border-color: #ff6b6b;
|
||||
}
|
||||
|
||||
.drag_overlay.disabled .drag_overlay_content {
|
||||
color: #ff6b6b;
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { useWorkflow, useWorkflowMessages, useWorkflowOperations, StartWorkflowRequest } from '../../../hooks/useWorkflows';
|
||||
import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
|
||||
import { useWorkflow, useWorkflowStatus, useWorkflowMessages, useWorkflowOperations, StartWorkflowRequest } from '../../../hooks/useWorkflows';
|
||||
|
||||
export interface WorkflowManagerState {
|
||||
currentWorkflowId: string | null;
|
||||
|
|
@ -14,25 +14,34 @@ export interface WorkflowManagerActions {
|
|||
loadWorkflow: (workflowId: string) => Promise<void>;
|
||||
startNewWorkflow: (prompt: string, fileIds: number[]) => Promise<string | null>;
|
||||
continueWorkflow: (prompt: string, fileIds: number[]) => Promise<boolean>;
|
||||
stopWorkflow: () => Promise<boolean>;
|
||||
clearWorkflow: () => void;
|
||||
refreshMessages: () => Promise<void>;
|
||||
setPolling: (enabled: boolean) => void;
|
||||
}
|
||||
|
||||
export function useWorkflowManager(initialWorkflowId?: string | null): [WorkflowManagerState, WorkflowManagerActions] {
|
||||
console.log('🏭 useWorkflowManager called with initialWorkflowId:', initialWorkflowId);
|
||||
|
||||
// Core state
|
||||
const [currentWorkflowId, setCurrentWorkflowId] = useState<string | null>(initialWorkflowId || null);
|
||||
const [isPolling, setIsPolling] = useState(false);
|
||||
const pollingIntervalRef = useRef<number | null>(null);
|
||||
|
||||
// Hook-based data fetching
|
||||
const { workflow, loading: workflowLoading, error: workflowError, refetch: refetchWorkflow } = useWorkflow(currentWorkflowId);
|
||||
const { workflow, loading: workflowLoading, error: workflowError } = useWorkflow(currentWorkflowId);
|
||||
const { status: workflowStatus, loading: statusLoading, error: statusError, refetch: refetchStatus } = useWorkflowStatus(currentWorkflowId);
|
||||
const { messages, loading: messagesLoading, error: messagesError, refetch: refetchMessages } = useWorkflowMessages(currentWorkflowId);
|
||||
const { startWorkflow } = useWorkflowOperations();
|
||||
const { startWorkflow, stopWorkflow: stopWorkflowRequest } = useWorkflowOperations();
|
||||
|
||||
// Use status for real-time updates, fallback to workflow for initial data
|
||||
const currentWorkflow = workflowStatus || workflow;
|
||||
|
||||
|
||||
|
||||
// Combined loading and error states
|
||||
const isLoading = workflowLoading || messagesLoading;
|
||||
const error = workflowError || messagesError;
|
||||
const isLoading = workflowLoading || statusLoading || messagesLoading;
|
||||
const error = workflowError || statusError || messagesError;
|
||||
|
||||
// Auto-polling for active workflows and message updates
|
||||
useEffect(() => {
|
||||
|
|
@ -40,15 +49,16 @@ export function useWorkflowManager(initialWorkflowId?: string | null): [Workflow
|
|||
console.log(`🔄 Starting auto-polling for workflow: ${currentWorkflowId}`);
|
||||
|
||||
pollingIntervalRef.current = window.setInterval(() => {
|
||||
console.log('🔄 Auto-polling workflow and messages...');
|
||||
// Always poll for messages when workflow is active
|
||||
refetchMessages();
|
||||
console.log('🔄 Auto-polling status...');
|
||||
// Always poll for status to detect workflow state changes
|
||||
refetchStatus();
|
||||
|
||||
// Also poll workflow status if we have workflow data
|
||||
if (workflow) {
|
||||
const isActive = ['running', 'processing', 'started'].includes(workflow.status);
|
||||
// Only poll messages if workflow is running
|
||||
if (currentWorkflow) {
|
||||
const isActive = ['running', 'processing', 'started'].includes(currentWorkflow.status);
|
||||
if (isActive) {
|
||||
refetchWorkflow();
|
||||
console.log('🔄 Workflow is active, also polling messages...');
|
||||
refetchMessages();
|
||||
}
|
||||
}
|
||||
}, 2000); // Poll every 2 seconds for smoother updates
|
||||
|
|
@ -61,7 +71,7 @@ export function useWorkflowManager(initialWorkflowId?: string | null): [Workflow
|
|||
pollingIntervalRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [isPolling, currentWorkflowId, workflow?.status, refetchWorkflow, refetchMessages]);
|
||||
}, [isPolling, currentWorkflowId, currentWorkflow?.status, refetchStatus, refetchMessages]);
|
||||
|
||||
// Actions
|
||||
const loadWorkflow = useCallback(async (workflowId: string) => {
|
||||
|
|
@ -129,6 +139,32 @@ export function useWorkflowManager(initialWorkflowId?: string | null): [Workflow
|
|||
}
|
||||
}, [currentWorkflowId, startWorkflow, refetchMessages]);
|
||||
|
||||
const stopWorkflow = useCallback(async (): Promise<boolean> => {
|
||||
if (!currentWorkflowId) {
|
||||
console.error('❌ Cannot stop workflow: no current workflow ID');
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log(`🛑 Stopping workflow ${currentWorkflowId}`);
|
||||
|
||||
const success = await stopWorkflowRequest(currentWorkflowId);
|
||||
|
||||
if (success) {
|
||||
console.log(`✅ Workflow ${currentWorkflowId} stopped`);
|
||||
setIsPolling(false); // Stop polling immediately
|
||||
|
||||
// Refresh workflow status to get the updated status
|
||||
setTimeout(() => {
|
||||
refetchStatus();
|
||||
}, 500);
|
||||
|
||||
return true;
|
||||
} else {
|
||||
console.error('❌ Failed to stop workflow');
|
||||
return false;
|
||||
}
|
||||
}, [currentWorkflowId, stopWorkflowRequest, refetchStatus]);
|
||||
|
||||
const clearWorkflow = useCallback(() => {
|
||||
console.log('🧹 Clearing workflow');
|
||||
setCurrentWorkflowId(null);
|
||||
|
|
@ -145,50 +181,57 @@ export function useWorkflowManager(initialWorkflowId?: string | null): [Workflow
|
|||
setIsPolling(enabled);
|
||||
}, []);
|
||||
|
||||
// Initialize workflow on mount if provided
|
||||
// Sync with external workflow ID changes
|
||||
useEffect(() => {
|
||||
if (initialWorkflowId && initialWorkflowId !== currentWorkflowId) {
|
||||
loadWorkflow(initialWorkflowId);
|
||||
}
|
||||
}, [initialWorkflowId, currentWorkflowId, loadWorkflow]);
|
||||
|
||||
// Auto-enable polling when workflow becomes active, keep polling until completed
|
||||
useEffect(() => {
|
||||
if (currentWorkflowId && workflow) {
|
||||
const isActive = ['running', 'processing', 'started'].includes(workflow.status);
|
||||
const isCompleted = ['completed', 'failed', 'stopped'].includes(workflow.status);
|
||||
|
||||
if (isActive) {
|
||||
console.log(`🟢 Workflow ${currentWorkflowId} is active (${workflow.status}), enabling polling`);
|
||||
setIsPolling(true);
|
||||
} else if (isCompleted) {
|
||||
console.log(`🔴 Workflow ${currentWorkflowId} is completed (${workflow.status}), disabling polling`);
|
||||
if (initialWorkflowId !== currentWorkflowId) {
|
||||
if (initialWorkflowId) {
|
||||
console.log(`🔄 useWorkflowManager: Loading workflow ${initialWorkflowId}`);
|
||||
loadWorkflow(initialWorkflowId);
|
||||
} else {
|
||||
console.log(`🧹 useWorkflowManager: Clearing workflow due to null initialWorkflowId`);
|
||||
setCurrentWorkflowId(null);
|
||||
setIsPolling(false);
|
||||
}
|
||||
} else if (currentWorkflowId && !workflow) {
|
||||
// If we have a workflow ID but no workflow data yet, start polling to get updates
|
||||
console.log(`⏳ Workflow ${currentWorkflowId} loaded, starting polling for updates`);
|
||||
setIsPolling(true);
|
||||
}
|
||||
}, [currentWorkflowId, workflow?.status]);
|
||||
}, [initialWorkflowId]);
|
||||
|
||||
// Auto-enable polling only for active workflows
|
||||
useEffect(() => {
|
||||
if (currentWorkflowId && currentWorkflow) {
|
||||
const isActive = ['running', 'processing', 'started'].includes(currentWorkflow.status);
|
||||
|
||||
if (isActive) {
|
||||
console.log(`🟢 Workflow ${currentWorkflowId} is active (${currentWorkflow.status}), enabling polling`);
|
||||
setIsPolling(true);
|
||||
} else {
|
||||
console.log(`🔴 Workflow ${currentWorkflowId} is inactive/completed (${currentWorkflow.status}), disabling polling`);
|
||||
setIsPolling(false);
|
||||
}
|
||||
} else {
|
||||
// No workflow ID or no workflow data, stop polling
|
||||
console.log(`🛑 No workflow or workflow data, disabling polling`);
|
||||
setIsPolling(false);
|
||||
}
|
||||
}, [currentWorkflowId, currentWorkflow?.status]);
|
||||
|
||||
const state: WorkflowManagerState = {
|
||||
currentWorkflowId,
|
||||
workflow,
|
||||
workflow: currentWorkflow,
|
||||
messages,
|
||||
isLoading,
|
||||
error,
|
||||
isPolling
|
||||
};
|
||||
|
||||
const actions: WorkflowManagerActions = {
|
||||
const actions: WorkflowManagerActions = useMemo(() => ({
|
||||
loadWorkflow,
|
||||
startNewWorkflow,
|
||||
continueWorkflow,
|
||||
stopWorkflow,
|
||||
clearWorkflow,
|
||||
refreshMessages,
|
||||
setPolling: setPollingEnabled
|
||||
};
|
||||
}), [loadWorkflow, startNewWorkflow, continueWorkflow, stopWorkflow, clearWorkflow, refreshMessages, setPollingEnabled]);
|
||||
|
||||
return [state, actions];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,6 +57,11 @@ export default {
|
|||
'dashboard.log.waiting': 'Workflow läuft... Warte auf Logs...',
|
||||
'dashboard.log.fetch_failed': 'Logs konnten nicht geladen werden',
|
||||
'dashboard.log.level.info': 'INFO',
|
||||
'dashboard.workflow_dropdown.loading': 'Laden...',
|
||||
'dashboard.workflow_dropdown.error': 'Fehler',
|
||||
'dashboard.workflow_dropdown.select_workflow': 'Workflow auswählen',
|
||||
'dashboard.workflow_dropdown.available_workflows': 'Verfügbare Workflows',
|
||||
'dashboard.workflow_dropdown.no_workflows': 'Keine Workflows verfügbar',
|
||||
|
||||
// Prompt Set
|
||||
'promptset.loading': 'Prompts werden geladen...',
|
||||
|
|
@ -182,6 +187,10 @@ export default {
|
|||
'chat.input.processing': 'Wird verarbeitet...',
|
||||
'chat.input.continue': 'Fortsetzen',
|
||||
'chat.input.send': 'Senden',
|
||||
'chat.input.stop': 'Stoppen',
|
||||
'chat.input.stopping': 'Wird gestoppt...',
|
||||
'chat.input.drop_files_here': 'Dateien hier ablegen zum Anhängen',
|
||||
'chat.input.drop_disabled': 'Datei-Ablage während Workflow deaktiviert',
|
||||
'chat.input.new_chat': 'Neuer Chat',
|
||||
'chat.input.using_prompt': 'Verwende Vorlage:',
|
||||
|
||||
|
|
|
|||
|
|
@ -57,6 +57,11 @@ export default {
|
|||
'dashboard.log.waiting': 'Workflow running... Waiting for logs...',
|
||||
'dashboard.log.fetch_failed': 'Failed to fetch logs',
|
||||
'dashboard.log.level.info': 'INFO',
|
||||
'dashboard.workflow_dropdown.loading': 'Loading...',
|
||||
'dashboard.workflow_dropdown.error': 'Error',
|
||||
'dashboard.workflow_dropdown.select_workflow': 'Select Workflow',
|
||||
'dashboard.workflow_dropdown.available_workflows': 'Available Workflows',
|
||||
'dashboard.workflow_dropdown.no_workflows': 'No workflows available',
|
||||
|
||||
// Prompt Set
|
||||
'promptset.loading': 'Loading prompts...',
|
||||
|
|
@ -183,6 +188,10 @@ export default {
|
|||
'chat.input.processing': 'Processing...',
|
||||
'chat.input.continue': 'Continue',
|
||||
'chat.input.send': 'Send',
|
||||
'chat.input.stop': 'Stop',
|
||||
'chat.input.stopping': 'Stopping...',
|
||||
'chat.input.drop_files_here': 'Drop files here to attach',
|
||||
'chat.input.drop_disabled': 'File drop disabled during workflow',
|
||||
'chat.input.new_chat': 'New Chat',
|
||||
'chat.input.using_prompt': 'Using prompt:',
|
||||
|
||||
|
|
|
|||
|
|
@ -57,6 +57,11 @@ export default {
|
|||
'dashboard.log.waiting': 'Workflow en cours... En attente des logs...',
|
||||
'dashboard.log.fetch_failed': 'Échec du chargement des logs',
|
||||
'dashboard.log.level.info': 'INFO',
|
||||
'dashboard.workflow_dropdown.loading': 'Chargement...',
|
||||
'dashboard.workflow_dropdown.error': 'Erreur',
|
||||
'dashboard.workflow_dropdown.select_workflow': 'Sélectionner un workflow',
|
||||
'dashboard.workflow_dropdown.available_workflows': 'Workflows disponibles',
|
||||
'dashboard.workflow_dropdown.no_workflows': 'Aucun workflow disponible',
|
||||
|
||||
// Prompt Set
|
||||
'promptset.loading': 'Chargement des prompts...',
|
||||
|
|
@ -182,6 +187,10 @@ export default {
|
|||
'chat.input.processing': 'Traitement...',
|
||||
'chat.input.continue': 'Continuer',
|
||||
'chat.input.send': 'Envoyer',
|
||||
'chat.input.stop': 'Arrêter',
|
||||
'chat.input.stopping': 'Arrêt...',
|
||||
'chat.input.drop_files_here': 'Déposez les fichiers ici pour les joindre',
|
||||
'chat.input.drop_disabled': 'Dépôt de fichiers désactivé pendant le workflow',
|
||||
'chat.input.new_chat': 'Nouveau Chat',
|
||||
'chat.input.using_prompt': 'Utilisation du modèle:',
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { useState, useCallback } from 'react';
|
||||
import { IoMdRefresh } from 'react-icons/io';
|
||||
import { useState, useCallback, useRef, useEffect } from 'react';
|
||||
import { IoMdRefresh, IoMdArrowDropdown } from 'react-icons/io';
|
||||
import { Prompt } from '../../hooks/usePrompts';
|
||||
import { useLanguage } from '../../contexts/LanguageContext';
|
||||
import { useWorkflows, Workflow } from '../../hooks/useWorkflows';
|
||||
import styles from './HomeStyles/Dashboard.module.css'
|
||||
import sharedStyles from '../../components/PageManager/pages.module.css';
|
||||
|
||||
|
|
@ -12,23 +13,54 @@ function Dashboard () {
|
|||
const [isChatExpanded, setIsChatExpanded] = useState(false);
|
||||
const [selectedPrompt, setSelectedPrompt] = useState<Prompt | null>(null);
|
||||
const [currentWorkflowId, setCurrentWorkflowId] = useState<string | null>(null);
|
||||
|
||||
console.log('🏠 Dashboard render with currentWorkflowId:', currentWorkflowId);
|
||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Fetch workflows for dropdown
|
||||
const { workflows, loading: workflowsLoading, error: workflowsError } = useWorkflows();
|
||||
|
||||
const handleChatToggleExpand = () => {
|
||||
setIsChatExpanded(!isChatExpanded);
|
||||
};
|
||||
|
||||
|
||||
|
||||
const handleWorkflowIdChange = useCallback((workflowId: string | null) => {
|
||||
console.log('🔄 Dashboard.handleWorkflowIdChange called with:', workflowId);
|
||||
setCurrentWorkflowId(workflowId);
|
||||
}, []);
|
||||
|
||||
const handleWorkflowResume = useCallback((workflowId: string) => {
|
||||
// Set the workflow ID to resume it
|
||||
const handleWorkflowSelect = useCallback((workflowId: string) => {
|
||||
console.log('📋 Dashboard.handleWorkflowSelect called with:', workflowId);
|
||||
// Set the workflow ID when selected from dropdown
|
||||
setCurrentWorkflowId(workflowId);
|
||||
// Switch to Chat Area tab to show the resumed workflow
|
||||
setIsDropdownOpen(false);
|
||||
}, []);
|
||||
|
||||
// Close dropdown when clicking outside
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
||||
setIsDropdownOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Helper functions for workflow dropdown
|
||||
const getWorkflowDisplayName = (workflow: Workflow) => {
|
||||
// Use name first, then title, then fallback to id (first 8 chars)
|
||||
return workflow.name || workflow.title || `${workflow.id.substring(0, 8)}...`;
|
||||
};
|
||||
|
||||
const formatWorkflowId = (id: string) => {
|
||||
return `${id.substring(0, 8)}...`;
|
||||
};
|
||||
|
||||
const handleResetWorkflow = useCallback(() => {
|
||||
setCurrentWorkflowId(null);
|
||||
setSelectedPrompt(null);
|
||||
|
|
@ -48,23 +80,139 @@ function Dashboard () {
|
|||
<div className={sharedStyles.pageHeader}>
|
||||
<h1 className={sharedStyles.pageTitle}>{t('nav.dashboard')}</h1>
|
||||
<div style={{ display: 'flex', gap: '15px', alignItems: 'center' }}>
|
||||
{currentWorkflowId && (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', padding: '8px 16px', backgroundColor: 'var(--color-gray-disabled)', borderRadius: '20px' }}>
|
||||
<span style={{ fontSize: '14px', color: 'var(--color-text)', fontWeight: '500' }}>
|
||||
{t('dashboard.log.workflow')}: {displayWorkflowId}
|
||||
</span>
|
||||
{currentWorkflowId ? (
|
||||
<>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', padding: '8px 16px', backgroundColor: 'var(--color-gray-disabled)', borderRadius: '20px' }}>
|
||||
<span style={{ fontSize: '14px', color: 'var(--color-text)', fontWeight: '500' }}>
|
||||
{t('dashboard.log.workflow')}: {displayWorkflowId}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
className={sharedStyles.secondaryButton}
|
||||
onClick={handleResetWorkflow}
|
||||
aria-label="Reset workflow"
|
||||
>
|
||||
<span className={sharedStyles.buttonIcon}><IoMdRefresh /></span>
|
||||
Reset
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<div style={{ position: 'relative', display: 'inline-block' }} ref={dropdownRef}>
|
||||
<button
|
||||
className={sharedStyles.secondaryButton}
|
||||
onClick={() => setIsDropdownOpen(!isDropdownOpen)}
|
||||
disabled={workflowsLoading}
|
||||
aria-expanded={isDropdownOpen}
|
||||
aria-haspopup="listbox"
|
||||
style={{ minWidth: '180px', justifyContent: 'space-between' }}
|
||||
>
|
||||
<span>
|
||||
{workflowsLoading
|
||||
? t('dashboard.workflow_dropdown.loading')
|
||||
: workflowsError
|
||||
? t('dashboard.workflow_dropdown.error')
|
||||
: t('dashboard.workflow_dropdown.select_workflow')
|
||||
}
|
||||
</span>
|
||||
<IoMdArrowDropdown
|
||||
className={sharedStyles.buttonIcon}
|
||||
style={{
|
||||
transform: isDropdownOpen ? 'rotate(180deg)' : 'rotate(0deg)',
|
||||
transition: 'transform 0.2s ease'
|
||||
}}
|
||||
/>
|
||||
</button>
|
||||
|
||||
{isDropdownOpen && !workflowsLoading && !workflowsError && (
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: '100%',
|
||||
left: 0,
|
||||
right: 0,
|
||||
marginTop: '4px',
|
||||
backgroundColor: 'var(--color-bg)',
|
||||
border: '1px solid var(--color-primary)',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
|
||||
zIndex: 1000,
|
||||
maxHeight: '300px',
|
||||
overflowY: 'auto'
|
||||
}}>
|
||||
<div style={{
|
||||
padding: '12px 16px',
|
||||
fontSize: '12px',
|
||||
fontWeight: 600,
|
||||
color: 'var(--color-text)',
|
||||
backgroundColor: 'var(--color-gray-disabled)',
|
||||
borderBottom: '1px solid var(--color-primary)',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.5px'
|
||||
}}>
|
||||
{t('dashboard.workflow_dropdown.available_workflows')}
|
||||
</div>
|
||||
|
||||
{workflows.length === 0 ? (
|
||||
<div style={{
|
||||
padding: '12px 16px',
|
||||
fontSize: '14px',
|
||||
color: 'var(--color-text-secondary)',
|
||||
fontStyle: 'italic'
|
||||
}}>
|
||||
{t('dashboard.workflow_dropdown.no_workflows')}
|
||||
</div>
|
||||
) : (
|
||||
workflows.map((workflow) => (
|
||||
<button
|
||||
key={workflow.id}
|
||||
onClick={() => handleWorkflowSelect(workflow.id)}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '12px 16px',
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
textAlign: 'left',
|
||||
cursor: 'pointer',
|
||||
fontFamily: 'var(--font-family)',
|
||||
transition: 'background-color 0.2s ease',
|
||||
borderBottom: '1px solid var(--color-gray-disabled)'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.backgroundColor = 'var(--color-gray-disabled)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.backgroundColor = 'transparent';
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
|
||||
<span style={{
|
||||
fontSize: '14px',
|
||||
fontWeight: 500,
|
||||
color: 'var(--color-text)'
|
||||
}}>
|
||||
{getWorkflowDisplayName(workflow)}
|
||||
</span>
|
||||
<span style={{
|
||||
fontSize: '12px',
|
||||
color: 'var(--color-text-secondary)',
|
||||
fontFamily: 'monospace'
|
||||
}}>
|
||||
ID: {formatWorkflowId(workflow.id)}
|
||||
</span>
|
||||
<span style={{
|
||||
fontSize: '12px',
|
||||
color: 'var(--color-text-secondary)',
|
||||
textTransform: 'capitalize'
|
||||
}}>
|
||||
{workflow.status}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{currentWorkflowId && (
|
||||
<button
|
||||
className={sharedStyles.secondaryButton}
|
||||
onClick={handleResetWorkflow}
|
||||
aria-label="Reset workflow"
|
||||
>
|
||||
<span className={sharedStyles.buttonIcon}><IoMdRefresh /></span>
|
||||
Reset
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={sharedStyles.horizontalDivider}></div>
|
||||
|
|
@ -78,7 +226,7 @@ function Dashboard () {
|
|||
selectedPrompt={selectedPrompt}
|
||||
onPromptUsed={() => setSelectedPrompt(null)}
|
||||
onWorkflowIdChange={handleWorkflowIdChange}
|
||||
onWorkflowResume={handleWorkflowResume}
|
||||
currentWorkflowId={currentWorkflowId}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in a new issue