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 { 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';
|
import styles from './DashboardChatAreaStyles/DashboardChat.module.css';
|
||||||
|
|
||||||
|
|
@ -13,7 +12,7 @@ interface DashboardChatProps {
|
||||||
onPromptUsed?: () => void;
|
onPromptUsed?: () => void;
|
||||||
onWorkflowIdChange?: (workflowId: string | null) => void;
|
onWorkflowIdChange?: (workflowId: string | null) => void;
|
||||||
onWorkflowCompletedChange?: (completed: boolean) => void;
|
onWorkflowCompletedChange?: (completed: boolean) => void;
|
||||||
onWorkflowResume?: (workflowId: string) => void;
|
currentWorkflowId?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DashboardChat: React.FC<DashboardChatProps> = ({
|
const DashboardChat: React.FC<DashboardChatProps> = ({
|
||||||
|
|
@ -21,23 +20,11 @@ const DashboardChat: React.FC<DashboardChatProps> = ({
|
||||||
onPromptUsed,
|
onPromptUsed,
|
||||||
onWorkflowIdChange,
|
onWorkflowIdChange,
|
||||||
onWorkflowCompletedChange,
|
onWorkflowCompletedChange,
|
||||||
onWorkflowResume
|
currentWorkflowId
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useLanguage();
|
// Pass the current workflow ID directly to the chat area
|
||||||
const [resumeWorkflowId, setResumeWorkflowId] = useState<string | null>(null);
|
// This handles both dropdown selection and workflow resume scenarios
|
||||||
|
const effectiveWorkflowId = currentWorkflowId;
|
||||||
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);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.dashboard_chat}>
|
<div className={styles.dashboard_chat}>
|
||||||
|
|
@ -46,7 +33,7 @@ const DashboardChat: React.FC<DashboardChatProps> = ({
|
||||||
onPromptUsed={onPromptUsed}
|
onPromptUsed={onPromptUsed}
|
||||||
onWorkflowIdChange={onWorkflowIdChange}
|
onWorkflowIdChange={onWorkflowIdChange}
|
||||||
onWorkflowCompletedChange={onWorkflowCompletedChange}
|
onWorkflowCompletedChange={onWorkflowCompletedChange}
|
||||||
resumeWorkflowId={resumeWorkflowId}
|
resumeWorkflowId={effectiveWorkflowId}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useState } from "react";
|
import React, { useState, useRef } from "react";
|
||||||
import MessageList from "./DashboardChatAreaMessageList";
|
import MessageList from "./DashboardChatAreaMessageList";
|
||||||
import FilePreview from "./DashboardChatAreaFilePreview";
|
import FilePreview from "./DashboardChatAreaFilePreview";
|
||||||
import InputArea from "./DashboardChatAreaInput";
|
import InputArea from "./DashboardChatAreaInput";
|
||||||
|
|
@ -20,14 +20,23 @@ const DashboardChatArea: React.FC<DashboardChatAreaProps> = ({
|
||||||
const [selectedFile, setSelectedFile] = useState<any>(null);
|
const [selectedFile, setSelectedFile] = useState<any>(null);
|
||||||
const [attachedFiles, setAttachedFiles] = useState<any[]>([]);
|
const [attachedFiles, setAttachedFiles] = useState<any[]>([]);
|
||||||
|
|
||||||
|
// Track the last resumeWorkflowId to prevent feedback loops
|
||||||
|
const lastResumeWorkflowIdRef = useRef<string | null>(resumeWorkflowId);
|
||||||
|
|
||||||
// Centralized workflow management
|
// Centralized workflow management
|
||||||
const [workflowState, workflowActions] = useWorkflowManager(resumeWorkflowId);
|
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(() => {
|
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);
|
onWorkflowIdChange(workflowState.currentWorkflowId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update the ref to track the current resumeWorkflowId
|
||||||
|
lastResumeWorkflowIdRef.current = resumeWorkflowId;
|
||||||
}, [workflowState.currentWorkflowId, onWorkflowIdChange, resumeWorkflowId]);
|
}, [workflowState.currentWorkflowId, onWorkflowIdChange, resumeWorkflowId]);
|
||||||
|
|
||||||
// Notify parent when workflow is completed
|
// Notify parent when workflow is completed
|
||||||
|
|
@ -38,13 +47,7 @@ const DashboardChatArea: React.FC<DashboardChatAreaProps> = ({
|
||||||
}
|
}
|
||||||
}, [workflowState.workflow?.status, onWorkflowCompletedChange]);
|
}, [workflowState.workflow?.status, onWorkflowCompletedChange]);
|
||||||
|
|
||||||
// Auto-load workflow when resumeWorkflowId changes externally
|
// Note: useWorkflowManager handles resumeWorkflowId changes automatically
|
||||||
React.useEffect(() => {
|
|
||||||
if (resumeWorkflowId && resumeWorkflowId !== workflowState.currentWorkflowId) {
|
|
||||||
console.log(`🔄 Loading workflow from external prop: ${resumeWorkflowId}`);
|
|
||||||
workflowActions.loadWorkflow(resumeWorkflowId);
|
|
||||||
}
|
|
||||||
}, [resumeWorkflowId, workflowState.currentWorkflowId, workflowActions]);
|
|
||||||
|
|
||||||
// No resizing functionality needed
|
// No resizing functionality needed
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,9 @@ const InputArea: React.FC<InputAreaProps> = ({
|
||||||
const [isSending, setIsSending] = useState(false);
|
const [isSending, setIsSending] = useState(false);
|
||||||
const [sendError, setSendError] = useState<string | null>(null);
|
const [sendError, setSendError] = useState<string | null>(null);
|
||||||
const [isFocused, setIsFocused] = useState(false);
|
const [isFocused, setIsFocused] = useState(false);
|
||||||
|
const [isDragOver, setIsDragOver] = useState(false);
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
const dropZoneRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
// Always use external attached files from parent component
|
// Always use external attached files from parent component
|
||||||
const currentAttachedFiles = externalAttachedFiles;
|
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) => {
|
const handleKeyPress = (e: React.KeyboardEvent) => {
|
||||||
if (e.key === 'Enter' && !e.shiftKey) {
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
e.preventDefault();
|
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 &&
|
const isWorkflowActive = workflowState.workflow &&
|
||||||
['running', 'processing', 'started'].includes(workflowState.workflow.status);
|
['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
|
// Determine if label should be in focused/moved state
|
||||||
const shouldLabelBeFocused = isFocused || inputValue.trim().length > 0;
|
const shouldLabelBeFocused = isFocused || inputValue.trim().length > 0;
|
||||||
|
|
@ -147,7 +231,14 @@ const InputArea: React.FC<InputAreaProps> = ({
|
||||||
: t('chat.input.enter_message');
|
: t('chat.input.enter_message');
|
||||||
|
|
||||||
return (
|
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 */}
|
{/* Error messages */}
|
||||||
{(sendError || workflowState.error) && (
|
{(sendError || workflowState.error) && (
|
||||||
|
|
@ -156,6 +247,23 @@ const InputArea: React.FC<InputAreaProps> = ({
|
||||||
</div>
|
</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 */}
|
{/* Show attached files count */}
|
||||||
{currentAttachedFiles.length > 0 && (
|
{currentAttachedFiles.length > 0 && (
|
||||||
<div className={styles.attached_files_count}>
|
<div className={styles.attached_files_count}>
|
||||||
|
|
@ -175,14 +283,13 @@ const InputArea: React.FC<InputAreaProps> = ({
|
||||||
value={inputValue}
|
value={inputValue}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setInputValue(e.target.value);
|
setInputValue(e.target.value);
|
||||||
// Trigger resize on next frame to ensure DOM is updated
|
|
||||||
setTimeout(adjustTextareaHeight, 0);
|
setTimeout(adjustTextareaHeight, 0);
|
||||||
}}
|
}}
|
||||||
onKeyPress={handleKeyPress}
|
onKeyPress={handleKeyPress}
|
||||||
onFocus={() => setIsFocused(true)}
|
onFocus={() => setIsFocused(true)}
|
||||||
onBlur={() => setIsFocused(false)}
|
onBlur={() => setIsFocused(false)}
|
||||||
placeholder=""
|
placeholder=""
|
||||||
disabled={isSending || isWorkflowActive}
|
disabled={isSending || shouldShowStopButton}
|
||||||
className={styles.message_textarea}
|
className={styles.message_textarea}
|
||||||
rows={4}
|
rows={4}
|
||||||
/>
|
/>
|
||||||
|
|
@ -191,27 +298,40 @@ const InputArea: React.FC<InputAreaProps> = ({
|
||||||
<div className={styles.input_actions_row}>
|
<div className={styles.input_actions_row}>
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowFilePopup(true)}
|
onClick={() => setShowFilePopup(true)}
|
||||||
disabled={isSending || isWorkflowActive}
|
disabled={isSending || shouldShowStopButton}
|
||||||
className={sharedStyles.button_secondary}
|
className={`${sharedStyles.button_secondary} ${
|
||||||
|
(isSending || shouldShowStopButton) ? styles.disabled : ''
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
{t('chat.input.attach_files')}
|
{t('chat.input.attach_files')}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
{shouldShowStopButton ? (
|
||||||
onClick={handleSend}
|
<button
|
||||||
disabled={!inputValue.trim() || isSending || isWorkflowActive}
|
onClick={handleStop}
|
||||||
className={`${sharedStyles.button_primary} ${
|
disabled={isSending}
|
||||||
(!inputValue.trim() || isSending || isWorkflowActive)
|
className={`${sharedStyles.button_primary} ${
|
||||||
? styles.disabled
|
isSending ? styles.disabled : styles.enabled
|
||||||
: styles.enabled
|
}`}
|
||||||
}`}
|
>
|
||||||
>
|
{isSending ? t('chat.input.stopping') : t('chat.input.stop')}
|
||||||
{isSending ? t('chat.input.sending') :
|
</button>
|
||||||
isWorkflowActive ? t('chat.input.processing') :
|
) : (
|
||||||
workflowState.currentWorkflowId ? t('chat.input.continue') : t('chat.input.send')}
|
<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
|
<button
|
||||||
onClick={() => workflowActions.clearWorkflow()}
|
onClick={() => workflowActions.clearWorkflow()}
|
||||||
className={styles.new_chat_button}
|
className={styles.new_chat_button}
|
||||||
|
|
|
||||||
|
|
@ -135,3 +135,55 @@
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--color-gray);
|
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 { useState, useEffect, useCallback, useRef, useMemo } from 'react';
|
||||||
import { useWorkflow, useWorkflowMessages, useWorkflowOperations, StartWorkflowRequest } from '../../../hooks/useWorkflows';
|
import { useWorkflow, useWorkflowStatus, useWorkflowMessages, useWorkflowOperations, StartWorkflowRequest } from '../../../hooks/useWorkflows';
|
||||||
|
|
||||||
export interface WorkflowManagerState {
|
export interface WorkflowManagerState {
|
||||||
currentWorkflowId: string | null;
|
currentWorkflowId: string | null;
|
||||||
|
|
@ -14,25 +14,34 @@ export interface WorkflowManagerActions {
|
||||||
loadWorkflow: (workflowId: string) => Promise<void>;
|
loadWorkflow: (workflowId: string) => Promise<void>;
|
||||||
startNewWorkflow: (prompt: string, fileIds: number[]) => Promise<string | null>;
|
startNewWorkflow: (prompt: string, fileIds: number[]) => Promise<string | null>;
|
||||||
continueWorkflow: (prompt: string, fileIds: number[]) => Promise<boolean>;
|
continueWorkflow: (prompt: string, fileIds: number[]) => Promise<boolean>;
|
||||||
|
stopWorkflow: () => Promise<boolean>;
|
||||||
clearWorkflow: () => void;
|
clearWorkflow: () => void;
|
||||||
refreshMessages: () => Promise<void>;
|
refreshMessages: () => Promise<void>;
|
||||||
setPolling: (enabled: boolean) => void;
|
setPolling: (enabled: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useWorkflowManager(initialWorkflowId?: string | null): [WorkflowManagerState, WorkflowManagerActions] {
|
export function useWorkflowManager(initialWorkflowId?: string | null): [WorkflowManagerState, WorkflowManagerActions] {
|
||||||
|
console.log('🏭 useWorkflowManager called with initialWorkflowId:', initialWorkflowId);
|
||||||
|
|
||||||
// Core state
|
// Core state
|
||||||
const [currentWorkflowId, setCurrentWorkflowId] = useState<string | null>(initialWorkflowId || null);
|
const [currentWorkflowId, setCurrentWorkflowId] = useState<string | null>(initialWorkflowId || null);
|
||||||
const [isPolling, setIsPolling] = useState(false);
|
const [isPolling, setIsPolling] = useState(false);
|
||||||
const pollingIntervalRef = useRef<number | null>(null);
|
const pollingIntervalRef = useRef<number | null>(null);
|
||||||
|
|
||||||
// Hook-based data fetching
|
// 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 { 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
|
// Combined loading and error states
|
||||||
const isLoading = workflowLoading || messagesLoading;
|
const isLoading = workflowLoading || statusLoading || messagesLoading;
|
||||||
const error = workflowError || messagesError;
|
const error = workflowError || statusError || messagesError;
|
||||||
|
|
||||||
// Auto-polling for active workflows and message updates
|
// Auto-polling for active workflows and message updates
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -40,15 +49,16 @@ export function useWorkflowManager(initialWorkflowId?: string | null): [Workflow
|
||||||
console.log(`🔄 Starting auto-polling for workflow: ${currentWorkflowId}`);
|
console.log(`🔄 Starting auto-polling for workflow: ${currentWorkflowId}`);
|
||||||
|
|
||||||
pollingIntervalRef.current = window.setInterval(() => {
|
pollingIntervalRef.current = window.setInterval(() => {
|
||||||
console.log('🔄 Auto-polling workflow and messages...');
|
console.log('🔄 Auto-polling status...');
|
||||||
// Always poll for messages when workflow is active
|
// Always poll for status to detect workflow state changes
|
||||||
refetchMessages();
|
refetchStatus();
|
||||||
|
|
||||||
// Also poll workflow status if we have workflow data
|
// Only poll messages if workflow is running
|
||||||
if (workflow) {
|
if (currentWorkflow) {
|
||||||
const isActive = ['running', 'processing', 'started'].includes(workflow.status);
|
const isActive = ['running', 'processing', 'started'].includes(currentWorkflow.status);
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
refetchWorkflow();
|
console.log('🔄 Workflow is active, also polling messages...');
|
||||||
|
refetchMessages();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, 2000); // Poll every 2 seconds for smoother updates
|
}, 2000); // Poll every 2 seconds for smoother updates
|
||||||
|
|
@ -61,7 +71,7 @@ export function useWorkflowManager(initialWorkflowId?: string | null): [Workflow
|
||||||
pollingIntervalRef.current = null;
|
pollingIntervalRef.current = null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [isPolling, currentWorkflowId, workflow?.status, refetchWorkflow, refetchMessages]);
|
}, [isPolling, currentWorkflowId, currentWorkflow?.status, refetchStatus, refetchMessages]);
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
const loadWorkflow = useCallback(async (workflowId: string) => {
|
const loadWorkflow = useCallback(async (workflowId: string) => {
|
||||||
|
|
@ -129,6 +139,32 @@ export function useWorkflowManager(initialWorkflowId?: string | null): [Workflow
|
||||||
}
|
}
|
||||||
}, [currentWorkflowId, startWorkflow, refetchMessages]);
|
}, [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(() => {
|
const clearWorkflow = useCallback(() => {
|
||||||
console.log('🧹 Clearing workflow');
|
console.log('🧹 Clearing workflow');
|
||||||
setCurrentWorkflowId(null);
|
setCurrentWorkflowId(null);
|
||||||
|
|
@ -145,50 +181,57 @@ export function useWorkflowManager(initialWorkflowId?: string | null): [Workflow
|
||||||
setIsPolling(enabled);
|
setIsPolling(enabled);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Initialize workflow on mount if provided
|
// Sync with external workflow ID changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (initialWorkflowId && initialWorkflowId !== currentWorkflowId) {
|
if (initialWorkflowId !== currentWorkflowId) {
|
||||||
loadWorkflow(initialWorkflowId);
|
if (initialWorkflowId) {
|
||||||
}
|
console.log(`🔄 useWorkflowManager: Loading workflow ${initialWorkflowId}`);
|
||||||
}, [initialWorkflowId, currentWorkflowId, loadWorkflow]);
|
loadWorkflow(initialWorkflowId);
|
||||||
|
} else {
|
||||||
// Auto-enable polling when workflow becomes active, keep polling until completed
|
console.log(`🧹 useWorkflowManager: Clearing workflow due to null initialWorkflowId`);
|
||||||
useEffect(() => {
|
setCurrentWorkflowId(null);
|
||||||
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`);
|
|
||||||
setIsPolling(false);
|
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 = {
|
const state: WorkflowManagerState = {
|
||||||
currentWorkflowId,
|
currentWorkflowId,
|
||||||
workflow,
|
workflow: currentWorkflow,
|
||||||
messages,
|
messages,
|
||||||
isLoading,
|
isLoading,
|
||||||
error,
|
error,
|
||||||
isPolling
|
isPolling
|
||||||
};
|
};
|
||||||
|
|
||||||
const actions: WorkflowManagerActions = {
|
const actions: WorkflowManagerActions = useMemo(() => ({
|
||||||
loadWorkflow,
|
loadWorkflow,
|
||||||
startNewWorkflow,
|
startNewWorkflow,
|
||||||
continueWorkflow,
|
continueWorkflow,
|
||||||
|
stopWorkflow,
|
||||||
clearWorkflow,
|
clearWorkflow,
|
||||||
refreshMessages,
|
refreshMessages,
|
||||||
setPolling: setPollingEnabled
|
setPolling: setPollingEnabled
|
||||||
};
|
}), [loadWorkflow, startNewWorkflow, continueWorkflow, stopWorkflow, clearWorkflow, refreshMessages, setPollingEnabled]);
|
||||||
|
|
||||||
return [state, actions];
|
return [state, actions];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,11 @@ export default {
|
||||||
'dashboard.log.waiting': 'Workflow läuft... Warte auf Logs...',
|
'dashboard.log.waiting': 'Workflow läuft... Warte auf Logs...',
|
||||||
'dashboard.log.fetch_failed': 'Logs konnten nicht geladen werden',
|
'dashboard.log.fetch_failed': 'Logs konnten nicht geladen werden',
|
||||||
'dashboard.log.level.info': 'INFO',
|
'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
|
// Prompt Set
|
||||||
'promptset.loading': 'Prompts werden geladen...',
|
'promptset.loading': 'Prompts werden geladen...',
|
||||||
|
|
@ -182,6 +187,10 @@ export default {
|
||||||
'chat.input.processing': 'Wird verarbeitet...',
|
'chat.input.processing': 'Wird verarbeitet...',
|
||||||
'chat.input.continue': 'Fortsetzen',
|
'chat.input.continue': 'Fortsetzen',
|
||||||
'chat.input.send': 'Senden',
|
'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.new_chat': 'Neuer Chat',
|
||||||
'chat.input.using_prompt': 'Verwende Vorlage:',
|
'chat.input.using_prompt': 'Verwende Vorlage:',
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,11 @@ export default {
|
||||||
'dashboard.log.waiting': 'Workflow running... Waiting for logs...',
|
'dashboard.log.waiting': 'Workflow running... Waiting for logs...',
|
||||||
'dashboard.log.fetch_failed': 'Failed to fetch logs',
|
'dashboard.log.fetch_failed': 'Failed to fetch logs',
|
||||||
'dashboard.log.level.info': 'INFO',
|
'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
|
// Prompt Set
|
||||||
'promptset.loading': 'Loading prompts...',
|
'promptset.loading': 'Loading prompts...',
|
||||||
|
|
@ -183,6 +188,10 @@ export default {
|
||||||
'chat.input.processing': 'Processing...',
|
'chat.input.processing': 'Processing...',
|
||||||
'chat.input.continue': 'Continue',
|
'chat.input.continue': 'Continue',
|
||||||
'chat.input.send': 'Send',
|
'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.new_chat': 'New Chat',
|
||||||
'chat.input.using_prompt': 'Using prompt:',
|
'chat.input.using_prompt': 'Using prompt:',
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,11 @@ export default {
|
||||||
'dashboard.log.waiting': 'Workflow en cours... En attente des logs...',
|
'dashboard.log.waiting': 'Workflow en cours... En attente des logs...',
|
||||||
'dashboard.log.fetch_failed': 'Échec du chargement des logs',
|
'dashboard.log.fetch_failed': 'Échec du chargement des logs',
|
||||||
'dashboard.log.level.info': 'INFO',
|
'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
|
// Prompt Set
|
||||||
'promptset.loading': 'Chargement des prompts...',
|
'promptset.loading': 'Chargement des prompts...',
|
||||||
|
|
@ -182,6 +187,10 @@ export default {
|
||||||
'chat.input.processing': 'Traitement...',
|
'chat.input.processing': 'Traitement...',
|
||||||
'chat.input.continue': 'Continuer',
|
'chat.input.continue': 'Continuer',
|
||||||
'chat.input.send': 'Envoyer',
|
'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.new_chat': 'Nouveau Chat',
|
||||||
'chat.input.using_prompt': 'Utilisation du modèle:',
|
'chat.input.using_prompt': 'Utilisation du modèle:',
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import { useState, useCallback } from 'react';
|
import { useState, useCallback, useRef, useEffect } from 'react';
|
||||||
import { IoMdRefresh } from 'react-icons/io';
|
import { IoMdRefresh, IoMdArrowDropdown } from 'react-icons/io';
|
||||||
import { Prompt } from '../../hooks/usePrompts';
|
import { Prompt } from '../../hooks/usePrompts';
|
||||||
import { useLanguage } from '../../contexts/LanguageContext';
|
import { useLanguage } from '../../contexts/LanguageContext';
|
||||||
|
import { useWorkflows, Workflow } from '../../hooks/useWorkflows';
|
||||||
import styles from './HomeStyles/Dashboard.module.css'
|
import styles from './HomeStyles/Dashboard.module.css'
|
||||||
import sharedStyles from '../../components/PageManager/pages.module.css';
|
import sharedStyles from '../../components/PageManager/pages.module.css';
|
||||||
|
|
||||||
|
|
@ -12,23 +13,54 @@ function Dashboard () {
|
||||||
const [isChatExpanded, setIsChatExpanded] = useState(false);
|
const [isChatExpanded, setIsChatExpanded] = useState(false);
|
||||||
const [selectedPrompt, setSelectedPrompt] = useState<Prompt | null>(null);
|
const [selectedPrompt, setSelectedPrompt] = useState<Prompt | null>(null);
|
||||||
const [currentWorkflowId, setCurrentWorkflowId] = useState<string | 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 = () => {
|
const handleChatToggleExpand = () => {
|
||||||
setIsChatExpanded(!isChatExpanded);
|
setIsChatExpanded(!isChatExpanded);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const handleWorkflowIdChange = useCallback((workflowId: string | null) => {
|
const handleWorkflowIdChange = useCallback((workflowId: string | null) => {
|
||||||
|
console.log('🔄 Dashboard.handleWorkflowIdChange called with:', workflowId);
|
||||||
setCurrentWorkflowId(workflowId);
|
setCurrentWorkflowId(workflowId);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleWorkflowResume = useCallback((workflowId: string) => {
|
const handleWorkflowSelect = useCallback((workflowId: string) => {
|
||||||
// Set the workflow ID to resume it
|
console.log('📋 Dashboard.handleWorkflowSelect called with:', workflowId);
|
||||||
|
// Set the workflow ID when selected from dropdown
|
||||||
setCurrentWorkflowId(workflowId);
|
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(() => {
|
const handleResetWorkflow = useCallback(() => {
|
||||||
setCurrentWorkflowId(null);
|
setCurrentWorkflowId(null);
|
||||||
setSelectedPrompt(null);
|
setSelectedPrompt(null);
|
||||||
|
|
@ -48,23 +80,139 @@ function Dashboard () {
|
||||||
<div className={sharedStyles.pageHeader}>
|
<div className={sharedStyles.pageHeader}>
|
||||||
<h1 className={sharedStyles.pageTitle}>{t('nav.dashboard')}</h1>
|
<h1 className={sharedStyles.pageTitle}>{t('nav.dashboard')}</h1>
|
||||||
<div style={{ display: 'flex', gap: '15px', alignItems: 'center' }}>
|
<div style={{ display: 'flex', gap: '15px', alignItems: 'center' }}>
|
||||||
{currentWorkflowId && (
|
{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' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', padding: '8px 16px', backgroundColor: 'var(--color-gray-disabled)', borderRadius: '20px' }}>
|
||||||
{t('dashboard.log.workflow')}: {displayWorkflowId}
|
<span style={{ fontSize: '14px', color: 'var(--color-text)', fontWeight: '500' }}>
|
||||||
</span>
|
{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>
|
</div>
|
||||||
)}
|
)}
|
||||||
{currentWorkflowId && (
|
|
||||||
<button
|
|
||||||
className={sharedStyles.secondaryButton}
|
|
||||||
onClick={handleResetWorkflow}
|
|
||||||
aria-label="Reset workflow"
|
|
||||||
>
|
|
||||||
<span className={sharedStyles.buttonIcon}><IoMdRefresh /></span>
|
|
||||||
Reset
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={sharedStyles.horizontalDivider}></div>
|
<div className={sharedStyles.horizontalDivider}></div>
|
||||||
|
|
@ -78,7 +226,7 @@ function Dashboard () {
|
||||||
selectedPrompt={selectedPrompt}
|
selectedPrompt={selectedPrompt}
|
||||||
onPromptUsed={() => setSelectedPrompt(null)}
|
onPromptUsed={() => setSelectedPrompt(null)}
|
||||||
onWorkflowIdChange={handleWorkflowIdChange}
|
onWorkflowIdChange={handleWorkflowIdChange}
|
||||||
onWorkflowResume={handleWorkflowResume}
|
currentWorkflowId={currentWorkflowId}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue