From b0826a3f9aa98dcbb894348257a2c8530f2803bc Mon Sep 17 00:00:00 2001 From: Ida Dittrich Date: Mon, 5 Jan 2026 15:12:47 +0100 Subject: [PATCH] feat:weiter chatbot implementiert --- src/api/chatbotApi.ts | 4 + .../ActionButtons/ActionButton.module.css | 1 - .../FormGeneratorControls.tsx | 13 +- .../FormGeneratorList.module.css | 143 ++++++++- .../FormGeneratorList/FormGeneratorList.tsx | 292 ++++++++++++++++-- src/core/PageManager/PageRenderer.tsx | 143 ++++++++- src/core/PageManager/data/pages/chatbot.ts | 9 + src/hooks/useChatbot.ts | 119 ++++++- src/styles/buttons.css | 8 +- 9 files changed, 664 insertions(+), 68 deletions(-) diff --git a/src/api/chatbotApi.ts b/src/api/chatbotApi.ts index c9a35f4..8876e1c 100644 --- a/src/api/chatbotApi.ts +++ b/src/api/chatbotApi.ts @@ -73,12 +73,16 @@ export async function startChatbotStreamApi( ): Promise { try { // Prepare request body + console.log('[startChatbotStreamApi] requestBody received:', JSON.stringify(requestBody, null, 2)); + const body: any = { prompt: requestBody.prompt, ...(requestBody.listFileId && requestBody.listFileId.length > 0 && { listFileId: requestBody.listFileId }), ...(requestBody.userLanguage && { userLanguage: requestBody.userLanguage }), ...(requestBody.metadata && { metadata: requestBody.metadata }) }; + + console.log('[startChatbotStreamApi] body being sent:', JSON.stringify(body, null, 2)); // Add workflowId to query params if provided const url = requestBody.workflowId diff --git a/src/components/FormGenerator/ActionButtons/ActionButton.module.css b/src/components/FormGenerator/ActionButtons/ActionButton.module.css index ea56b60..d7d214e 100644 --- a/src/components/FormGenerator/ActionButtons/ActionButton.module.css +++ b/src/components/FormGenerator/ActionButtons/ActionButton.module.css @@ -20,7 +20,6 @@ .actionButton:hover { background: var(--color-secondary-hover); - transform: translateY(-1px); } .actionButton:disabled { diff --git a/src/components/FormGenerator/FormGeneratorControls/FormGeneratorControls.tsx b/src/components/FormGenerator/FormGeneratorControls/FormGeneratorControls.tsx index dcaa9e8..0299707 100644 --- a/src/components/FormGenerator/FormGeneratorControls/FormGeneratorControls.tsx +++ b/src/components/FormGenerator/FormGeneratorControls/FormGeneratorControls.tsx @@ -64,7 +64,7 @@ export function FormGeneratorControls({ filterFocused, onFilterFocus, selectedCount, - displayData: _displayData, + displayData, onDeleteSingle, onDeleteMultiple, onRefresh, @@ -76,6 +76,9 @@ export function FormGeneratorControls({ }: FormGeneratorControlsProps) { const { t } = useLanguage(); + // Check if all items are selected + const allItemsSelected = selectedCount > 0 && displayData.length > 0 && selectedCount === displayData.length; + // Filter fields that are filterable const filterableFields = fields.filter(field => { if (field.type === 'readonly') return false; @@ -159,7 +162,8 @@ export function FormGeneratorControls({ {/* Delete Controls - Show when items are selected */} {selectable && selectedCount > 0 && (
- {selectedCount === 1 && onDeleteSingle && ( + {/* Show delete single only if exactly 1 item selected AND not all items */} + {selectedCount === 1 && !allItemsSelected && onDeleteSingle && ( diff --git a/src/components/FormGenerator/FormGeneratorList/FormGeneratorList.module.css b/src/components/FormGenerator/FormGeneratorList/FormGeneratorList.module.css index 7cdee30..a4d520c 100644 --- a/src/components/FormGenerator/FormGeneratorList/FormGeneratorList.module.css +++ b/src/components/FormGenerator/FormGeneratorList/FormGeneratorList.module.css @@ -20,6 +20,7 @@ max-height: none; flex: 1; padding-right: 0.5rem; + padding-left: 0.5rem; } .emptyList { @@ -107,6 +108,65 @@ .headerButtonWrapper { margin-left: auto; flex-shrink: 0; + display: flex; + align-items: center; + gap: 0.5rem; +} + +/* Style buttons inside headerButtonWrapper to match ActionButton styling */ +.headerButtonWrapper button { + display: flex; + align-items: center; + justify-content: center; + padding: 6px; + border: none; + border-radius: 50%; + font-size: 12px; + font-family: var(--font-family); + cursor: pointer; + transition: all 0.2s ease; + white-space: nowrap; + position: relative; + min-width: 28px; + min-height: 28px; + background: var(--color-secondary); + color: var(--color-bg); +} + +.headerButtonWrapper button:hover { + background: var(--color-secondary-hover); +} + +.headerButtonWrapper button:disabled { + opacity: 0.6; + cursor: not-allowed; + transform: none !important; +} + +.headerButtonWrapper button:focus { + outline: none; + box-shadow: 0 0 0 2px rgba(var(--color-secondary-rgb), 0.3); +} + +/* Style icons inside headerButtonWrapper buttons */ +.headerButtonWrapper button svg, +.headerButtonWrapper button .actionIcon { + font-size: 16px; + height: 16px; + width: 16px; + display: flex; + align-items: center; + justify-content: center; +} + +.headerDeleteButtonContainer { + flex-shrink: 0; + margin-left: 0.5rem; + position: relative; +} + +.headerDeleteButton { + flex-shrink: 0; } .listCount { @@ -165,7 +225,7 @@ flex-direction: row; align-items: center; justify-content: space-between; - padding: 0.875rem 0.75rem; + padding: 0.875rem 1rem; background: transparent; border: none; border-top: 1px solid var(--color-medium-gray); @@ -248,6 +308,17 @@ gap: 0.375rem; flex: 1; min-width: 0; + padding-left: 0; + margin-left: 0; +} + +.metadataFields { + display: flex; + flex-direction: row; + align-items: center; + gap: 0.625rem; + margin-top: 0.375rem; + flex-wrap: wrap; } .itemField { @@ -262,27 +333,69 @@ margin-bottom: 0; } -.itemField:nth-child(2), -.itemField:nth-child(3) { +.itemField:first-child .fieldValue { + font-size: 1rem; + font-weight: 600; + color: var(--color-text); +} + +.metadataFields .itemField { display: inline-flex; flex-direction: row; align-items: center; - margin-top: 0.375rem; + margin: 0; + flex-shrink: 0; } -.itemField:nth-child(2) { - margin-right: 0.625rem; -} - -.itemField:nth-child(3) { - margin-left: 0; -} - -.itemField:nth-child(2) .fieldValue, -.itemField:nth-child(3) .fieldValue { +.metadataFields .itemField .fieldValue { + font-size: 0.75rem; + font-weight: 400; + color: var(--color-text-secondary, #666); + opacity: 0.8; display: inline; } +/* Date field styling (first in metadata) */ +.metadataFields .itemField:first-child .fieldValue { + font-size: 0.75rem; + font-weight: 400; + color: var(--color-text-secondary, #666); + opacity: 0.8; +} + +/* Status Badge Styling */ +.statusBadge { + display: inline-block; + padding: 0.25rem 0.625rem; + border-radius: 12px; + font-size: 0.75rem; + font-weight: 500; + line-height: 1.4; + white-space: nowrap; + text-transform: capitalize; +} + +/* Status color variants */ +.statusBadge.completed { + background-color: rgba(34, 197, 94, 0.15); + color: #16a34a; +} + +.statusBadge.pending { + background-color: rgba(251, 191, 36, 0.15); + color: #d97706; +} + +.statusBadge.failed { + background-color: rgba(239, 68, 68, 0.15); + color: #dc2626; +} + +.statusBadge.active { + background-color: rgba(59, 130, 246, 0.15); + color: #2563eb; +} + .fieldLabel { display: none; } @@ -496,11 +609,9 @@ @keyframes slideInFromTop { from { opacity: 0; - transform: translateY(-10px); } to { opacity: 1; - transform: translateY(0); } } diff --git a/src/components/FormGenerator/FormGeneratorList/FormGeneratorList.tsx b/src/components/FormGenerator/FormGeneratorList/FormGeneratorList.tsx index fdd18bb..74096a0 100644 --- a/src/components/FormGenerator/FormGeneratorList/FormGeneratorList.tsx +++ b/src/components/FormGenerator/FormGeneratorList/FormGeneratorList.tsx @@ -1,6 +1,7 @@ import React, { useState, useMemo, useRef, useEffect } from 'react'; import { useLanguage } from '../../../providers/language/LanguageContext'; import styles from './FormGeneratorList.module.css'; +import actionButtonStyles from '../ActionButtons/ActionButton.module.css'; import { EditActionButton, DeleteActionButton, @@ -13,6 +14,7 @@ import { import { formatUnixTimestamp } from '../../../utils/time'; import TextField from '../../UiComponents/TextField/TextField'; import { FormGeneratorControls } from '../FormGeneratorControls'; +import { IoIosTrash, IoIosCheckmark, IoIosClose } from "react-icons/io"; import { isSelectType, isCheckboxType, @@ -185,6 +187,8 @@ export function FormGeneratorList>({ const [selectedItems, setSelectedItems] = useState>(new Set()); const [currentPage, setCurrentPage] = useState(1); const [currentPageSize, setCurrentPageSize] = useState(pageSize); + const [isConfirmingDelete, setIsConfirmingDelete] = useState(false); + const [isDeleting, setIsDeleting] = useState(false); // Check if backend pagination is supported const supportsBackendPagination = hookData?.refetch && typeof hookData.refetch === 'function'; @@ -243,6 +247,12 @@ export function FormGeneratorList>({ // Data is already filtered, sorted, and paginated by the backend const displayData = data; + + // Check if all items are selected + const allItemsSelected = selectedItems.size > 0 && displayData.length > 0 && selectedItems.size === displayData.length; + + // Check if any items are selected + const hasSelectedItems = selectedItems.size > 0; // Get pagination info from backend const totalPages = useMemo(() => { @@ -324,38 +334,164 @@ export function FormGeneratorList>({ } }; - // Handle delete single item - const handleDeleteSingle = (row: T, index: number) => { - if (onDelete) { - onDelete(row); - if (selectedItems.has(index)) { - const newSelected = new Set(selectedItems); - newSelected.delete(index); - setSelectedItems(newSelected); - if (onItemSelect) { - const selectedData = Array.from(newSelected).map(i => displayData[i]); - onItemSelect(selectedData); + // Handle delete multiple items click (show confirmation) + const handleDeleteMultipleClick = () => { + if (selectedItems.size === 0 || isDeleting) return; + setIsConfirmingDelete(true); + }; + + // Handle confirm delete + const handleConfirmDelete = async () => { + if (selectedItems.size === 0) { + setIsConfirmingDelete(false); + return; + } + + setIsDeleting(true); + setIsConfirmingDelete(false); + + try { + const selectedData = Array.from(selectedItems).map(i => displayData[i]); + console.log('Deleting items:', selectedData.length, 'items', selectedData); + + // Try to use hookData first (like DeleteActionButton does) + if (hookData) { + const handleDelete = hookData.handleDelete || hookData.handleDeleteMultiple; + const removeOptimistically = hookData.removeOptimistically || hookData.removeFileOptimistically; + const refetch = hookData.refetch; + const idField = 'id'; // Default ID field, could be made configurable + + if (handleDelete) { + console.log('Using hookData.handleDelete'); + + // Get IDs from selected items + const selectedIds = selectedData.map(row => (row as any)[idField]).filter(Boolean); + console.log('Selected IDs:', selectedIds); + + // Optimistically remove items from UI + if (removeOptimistically) { + selectedIds.forEach(id => removeOptimistically(id)); + } + + // Delete each item + const deletePromises = selectedIds.map(id => handleDelete(id)); + const results = await Promise.all(deletePromises); + + // Check if all deletions succeeded + const allSucceeded = results.every(result => result !== false); + + if (allSucceeded) { + // If we used optimistic removal, don't refetch immediately + if (!removeOptimistically && refetch) { + await refetch(); + } + } else { + // Some deletions failed, refetch to restore state + if (refetch) { + await refetch(); + } + throw new Error('Some items failed to delete'); + } + } else if (onDeleteMultiple) { + console.log('Using onDeleteMultiple prop'); + const result = onDeleteMultiple(selectedData) as any; + if (result && typeof result.then === 'function') { + await result; + } + } else if (onDelete) { + console.log('Using onDelete prop for each item'); + for (const row of selectedData) { + const result = onDelete(row) as any; + if (result && typeof result.then === 'function') { + await result; + } + } + } else { + console.warn('No delete handler found in hookData or props'); + alert('No delete handler configured'); + return; } + } else if (onDeleteMultiple) { + console.log('Using onDeleteMultiple prop (no hookData)'); + const result = onDeleteMultiple(selectedData) as any; + if (result && typeof result.then === 'function') { + await result; + } + } else if (onDelete) { + console.log('Using onDelete prop for each item (no hookData)'); + for (const row of selectedData) { + const result = onDelete(row) as any; + if (result && typeof result.then === 'function') { + await result; + } + } + } else { + console.warn('No delete handler provided'); + alert('No delete handler configured'); + return; } + + // Clear selection after deletion + setSelectedItems(new Set()); + onItemSelect?.([]); + console.log('Delete completed, selection cleared'); + } catch (error) { + console.error('Delete failed:', error); + alert(`Delete failed: ${error}`); + } finally { + setIsDeleting(false); } }; - // Handle delete multiple items - const handleDeleteMultiple = () => { - if (onDeleteMultiple && selectedItems.size > 0) { - const selectedData = Array.from(selectedItems).map(i => displayData[i]); - onDeleteMultiple(selectedData); - setSelectedItems(new Set()); - onItemSelect?.([]); - } + // Handle cancel delete + const handleCancelDelete = () => { + setIsConfirmingDelete(false); }; + // Handle clicks outside delete confirmation buttons + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (isConfirmingDelete) { + const target = event.target as HTMLElement; + if (!target.closest(`.${styles.headerDeleteButtonContainer}`)) { + setIsConfirmingDelete(false); + } + } + }; + + if (isConfirmingDelete) { + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + } + }, [isConfirmingDelete]); + // Handle page size change const handlePageSizeChange = (newPageSize: number) => { setCurrentPageSize(newPageSize); setCurrentPage(1); }; + // Get status badge class based on status value + const getStatusBadgeClass = (value: any): string => { + const statusValue = String(value || '').toLowerCase().trim(); + + if (statusValue === 'completed' || statusValue === 'success' || statusValue === 'done') { + return styles.completed; + } + if (statusValue === 'pending' || statusValue === 'waiting' || statusValue === 'in_progress' || statusValue === 'in progress') { + return styles.pending; + } + if (statusValue === 'failed' || statusValue === 'error' || statusValue === 'cancelled' || statusValue === 'canceled') { + return styles.failed; + } + if (statusValue === 'active' || statusValue === 'running') { + return styles.active; + } + return ''; + }; + // Format field value const formatFieldValue = (value: any, field: FieldConfig, row: T) => { if (field.formatter) { @@ -459,10 +595,27 @@ export function FormGeneratorList>({ // Render field input const renderFieldInput = (field: FieldConfig, value: any, row: T, _index: number) => { + const isStatusField = field.key.toLowerCase().includes('status'); + if (field.type === 'readonly' || !field.editable) { + const formattedValue = formatFieldValue(value, field, row); + + // Apply status badge styling for status fields + if (isStatusField) { + const statusClass = getStatusBadgeClass(value); + + return ( +
+ + {formattedValue} + +
+ ); + } + return (
- {formatFieldValue(value, field, row)} + {formattedValue}
); } @@ -515,7 +668,7 @@ export function FormGeneratorList>({ return (
- {(searchable || filterable || (selectable && selectedItems.size > 0)) && ( + {(searchable || filterable) && selectedItems.size === 0 && ( >({ onFilterFocus={handleFilterFocus} selectedCount={selectedItems.size} displayData={displayData} - onDeleteSingle={selectedItems.size === 1 && onDelete ? () => { - const selectedIndex = Array.from(selectedItems)[0]; - const selectedRow = displayData[selectedIndex]; - handleDeleteSingle(selectedRow, selectedIndex); - } : undefined} - onDeleteMultiple={(selectedItems.size > 1 || (selectedItems.size === displayData.length && displayData.length > 0)) && onDeleteMultiple ? handleDeleteMultiple : undefined} + onDeleteSingle={undefined} + onDeleteMultiple={undefined} onRefresh={onRefresh} searchable={searchable} filterable={filterable} @@ -573,6 +722,47 @@ export function FormGeneratorList>({ {title && data.length > 0 && ( ({data.length}) )} + {hasSelectedItems && (onDeleteMultiple || onDelete) && ( +
+ {isConfirmingDelete ? ( +
+ + +
+ ) : ( + + )} +
+ )} {headerButton && (
{headerButton} @@ -713,17 +903,61 @@ export function FormGeneratorList>({ {/* Fields */}
- {detectedFields.map(field => { + {detectedFields.map((field, fieldIndex) => { const cellValue = row[field.key]; const customClassName = field.cellClassName ? field.cellClassName(cellValue, row) : ''; + // First field (name) - render normally + if (fieldIndex === 0) { + return ( +
+ + {renderFieldInput(field, cellValue, row, fieldIndex)} +
+ ); + } + + // Second and third fields (date and status) - wrap in container + if (fieldIndex === 1) { + const nextField = detectedFields[2]; + const nextFieldValue = nextField ? row[nextField.key] : null; + + return ( +
+
+ + {renderFieldInput(field, cellValue, row, fieldIndex)} +
+ {nextField && ( +
+ + {renderFieldInput(nextField, nextFieldValue, row, 2)} +
+ )} +
+ ); + } + + // Skip third field if it was already rendered with second field + if (fieldIndex === 2 && detectedFields.length > 2) { + return null; + } + + // Any additional fields beyond the first three return (
- {renderFieldInput(field, cellValue, row)} + {renderFieldInput(field, cellValue, row, fieldIndex)}
); })} diff --git a/src/core/PageManager/PageRenderer.tsx b/src/core/PageManager/PageRenderer.tsx index b1c882d..4aae85f 100644 --- a/src/core/PageManager/PageRenderer.tsx +++ b/src/core/PageManager/PageRenderer.tsx @@ -1016,9 +1016,12 @@ const PageRenderer: React.FC = ({ } }; - // Check if we have file management + // Check if we have file management (dashboard style with workflowFiles) const hasFileManagement = !!(hookData.handleFileUpload && hookData.workflowFiles !== undefined); + // Check if we have chatbot file upload (simpler style with uploadedFiles) + const hasChatbotFileUpload = !!(hookData.handleFileUpload && hookData.uploadedFiles !== undefined); + // Check RBAC permissions for prompt selector and workflow mode selector // Show prompt selector if user has permission to view/read prompts (even if no prompts exist yet) const showPromptSelector = hookData.promptPermission && @@ -1291,6 +1294,130 @@ const PageRenderer: React.FC = ({ ); } + // Chatbot file upload layout (simpler than dashboard) + if (hasChatbotFileUpload) { + const uploadedFiles = hookData.uploadedFiles || []; + return ( +
+ {/* Input and buttons row */} +
+
+ +
+
+ { + const result = await hookData.handleFileUpload!(file); + // Error handling is done in the hook + } : async () => {}} + disabled={hookData.isSubmitting || false} + loading={hookData.uploadingFile || false} + variant="secondary" + size={config.buttonSize || 'md'} + multiple={true} + accept="*/*" + > + Upload + + +
+
+ + {/* Pending files display */} + {uploadedFiles.length > 0 && ( +
+ {uploadedFiles.map((file: { fileId: string; fileName: string }) => ( +
+ 📎 + + {file.fileName} + + +
+ ))} +
+ )} + + {/* Upload error display */} + {hookData.uploadError && ( +
+ {hookData.uploadError} +
+ )} +
+ ); + } + // Default layout without files (backward compatible) return (
= ({ } - // If the page has drag drop config and hook data with handleUpload, integrate them - if (hookData?.handleUpload) { + // If the page has drag drop config and hook data with handleUpload or handleFileUpload, integrate them + const uploadHandler = hookData?.handleUpload || hookData?.handleFileUpload; + if (uploadHandler) { return { ...pageData.dragDropConfig, onDrop: async (files: File[]) => { - try { - // Process each file through the hook's handleUpload function + // Process each file through the hook's upload function for (const file of files) { - if (hookData.handleUpload) { - - await hookData.handleUpload(file); - + if (uploadHandler) { + await uploadHandler(file); } } } catch (error) { diff --git a/src/core/PageManager/data/pages/chatbot.ts b/src/core/PageManager/data/pages/chatbot.ts index 83d8df2..2ed8ee6 100644 --- a/src/core/PageManager/data/pages/chatbot.ts +++ b/src/core/PageManager/data/pages/chatbot.ts @@ -66,6 +66,15 @@ export const chatbotPageData: GenericPageData = { preload: true, moduleEnabled: true, + // Drag and drop configuration + dragDropConfig: { + enabled: true, + accept: '*/*', // Accept all file types + multiple: true, // Allow multiple files + overlayText: 'Drop files here to attach', + overlaySubtext: 'You can also click the upload button' + }, + // Lifecycle hooks onActivate: async () => { if (import.meta.env.DEV) console.log('Chatbot activated - state preserved'); diff --git a/src/hooks/useChatbot.ts b/src/hooks/useChatbot.ts index df1659e..76549b0 100644 --- a/src/hooks/useChatbot.ts +++ b/src/hooks/useChatbot.ts @@ -1,5 +1,6 @@ import { useState, useCallback, useRef, useMemo, useEffect } from 'react'; import { useApiRequest } from './useApi'; +import api from '../api'; import { startChatbotStreamApi, stopChatbotApi, @@ -32,6 +33,13 @@ export function useChatbot() { const [isSubmitting, setIsSubmitting] = useState(false); const [error, setError] = useState(null); + // File upload state + const [pendingFileIds, setPendingFileIds] = useState([]); + const pendingFileIdsRef = useRef([]); // Ref to avoid closure issues + const [uploadingFile, setUploadingFile] = useState(false); + const [uploadError, setUploadError] = useState(null); + const [uploadedFiles, setUploadedFiles] = useState>([]); + // Chat history state const [threads, setThreads] = useState([]); const [selectedThreadId, setSelectedThreadId] = useState(null); @@ -340,6 +348,73 @@ export function useChatbot() { setInputValue(value); }, []); + // Handle file upload + const handleFileUpload = useCallback(async (file: File): Promise<{ success: boolean; data?: any }> => { + setUploadError(null); + setUploadingFile(true); + + try { + // Validate file before upload + if (!file || !file.name || file.name.trim() === '') { + throw new Error('Invalid file: File must have a valid name'); + } + + if (file.size === 0) { + throw new Error('Invalid file: File cannot be empty'); + } + + const formData = new FormData(); + formData.append('file', file); + + const response = await api.post('/api/files/upload', formData, { + headers: { + 'Content-Type': 'multipart/form-data', + } + }); + + const fileData = response.data; + + // Extract fileId from response + // Backend returns: { message: "...", file: { id: "...", ... }, duplicateType: "..." } + const fileId = fileData?.file?.id || fileData?.id || fileData?.fileId; + + if (!fileId) { + console.error('Upload response structure:', fileData); + throw new Error('Upload failed: No file ID returned from server'); + } + + // Extract file name from response (use storedFileName if available, otherwise original fileName) + const fileName = fileData?.file?.fileName || fileData?.storedFileName || file.name; + + // Add to pending file IDs and uploaded files list + setPendingFileIds(prev => { + const updated = [...prev, fileId]; + pendingFileIdsRef.current = updated; // Keep ref in sync + return updated; + }); + setUploadedFiles(prev => [...prev, { fileId, fileName }]); + + return { success: true, data: fileData }; + } catch (err: any) { + console.error('File upload failed:', err); + const errorMessage = err.message || 'Failed to upload file'; + setUploadError(errorMessage); + return { success: false, error: errorMessage }; + } finally { + setUploadingFile(false); + } + }, []); + + // Handle file remove (remove from pending list) + const handleFileRemove = useCallback((fileId: string) => { + setPendingFileIds(prev => { + const updated = prev.filter(id => id !== fileId); + pendingFileIdsRef.current = updated; // Keep ref in sync + return updated; + }); + setUploadedFiles(prev => prev.filter(f => f.fileId !== fileId)); + }, []); + // Stop chatbot workflow const stopChatbot = useCallback(async () => { if (!workflowId || !isRunning) { @@ -398,12 +473,31 @@ export function useChatbot() { const abortController = new AbortController(); streamAbortControllerRef.current = abortController; - // Prepare request body + // Use ref to get current file IDs (avoids closure issues) + const fileIdsToSend = pendingFileIdsRef.current.length > 0 + ? pendingFileIdsRef.current + : pendingFileIds; // Fallback to state if ref is empty + + // Log for debugging + console.log('[handleSubmit] pendingFileIds from state:', pendingFileIds); + console.log('[handleSubmit] pendingFileIds from ref:', pendingFileIdsRef.current); + console.log('[handleSubmit] fileIdsToSend:', fileIdsToSend); + const requestBody: StartChatbotRequest = { prompt: trimmedInput, userLanguage: 'en', ...(workflowId && { workflowId }) }; + + // Always include listFileId if there are any files + if (fileIdsToSend.length > 0) { + requestBody.listFileId = fileIdsToSend; + console.log('[handleSubmit] Added listFileId to requestBody:', fileIdsToSend); + } else { + console.warn('[handleSubmit] No file IDs to send! Check if files were uploaded correctly.'); + } + + console.log('[handleSubmit] Final requestBody:', JSON.stringify(requestBody, null, 2)); // Track if workflow was created in this request let workflowCreated = false; @@ -452,6 +546,10 @@ export function useChatbot() { if (!abortController.signal.aborted) { setIsRunning(false); setInputValue(''); // Clear input on completion + // Clear pending file IDs after successful submission (files are now part of conversation) + setPendingFileIds([]); + pendingFileIdsRef.current = []; // Clear ref too + setUploadedFiles([]); // Clear thinking message on completion if no final message was received setTimeout(() => { clearThinkingMessage(); @@ -474,7 +572,7 @@ export function useChatbot() { setIsSubmitting(false); streamAbortControllerRef.current = null; } - }, [inputValue, workflowId, isRunning, isSubmitting, stopChatbot, processChatDataItem, clearThinkingMessage, loadThreads]); + }, [inputValue, workflowId, isRunning, isSubmitting, stopChatbot, processChatDataItem, clearThinkingMessage, loadThreads, pendingFileIds]); // Delete a chatbot workflow const handleDeleteThread = useCallback(async (workflowIdToDelete: string): Promise => { @@ -534,6 +632,9 @@ export function useChatbot() { setSelectedThreadId(null); setError(null); setInputValue(''); + setPendingFileIds([]); + pendingFileIdsRef.current = []; + setUploadedFiles([]); thinkingLogsRef.current = []; thinkingMessageIdRef.current = null; clearProcessedMessages(); @@ -554,6 +655,9 @@ export function useChatbot() { setIsSubmitting(false); setError(null); setInputValue(''); + setPendingFileIds([]); + pendingFileIdsRef.current = []; + setUploadedFiles([]); thinkingLogsRef.current = []; thinkingMessageIdRef.current = null; clearProcessedMessages(); @@ -613,7 +717,16 @@ export function useChatbot() { stopChatbot, resetChatbot, startNewChat, - cleanup + cleanup, + + // File upload interface + handleFileUpload, + handleUpload: handleFileUpload, // Alias for compatibility with DragDropOverlay + handleFileRemove, + pendingFileIds, + uploadedFiles, + uploadingFile, + uploadError }; } diff --git a/src/styles/buttons.css b/src/styles/buttons.css index 96b2e26..7b5556c 100644 --- a/src/styles/buttons.css +++ b/src/styles/buttons.css @@ -96,7 +96,6 @@ .buttonPrimary:hover:not(:disabled) { background: var(--button-primary-bg-hover); - transform: translateY(-1px); } .buttonSecondary { @@ -105,8 +104,8 @@ } .buttonSecondary:hover:not(:disabled) { - background: var(--button-secondary-bg-hover); - transform: translateY(-1px); + background: var(--color-secondary-hover); + color: white; } .buttonDanger { @@ -116,7 +115,6 @@ .buttonDanger:hover:not(:disabled) { background: var(--button-danger-bg-hover); - transform: translateY(-1px); } .buttonSuccess { @@ -126,7 +124,6 @@ .buttonSuccess:hover:not(:disabled) { background: var(--button-success-bg-hover); - transform: translateY(-1px); } .buttonWarning { @@ -136,7 +133,6 @@ .buttonWarning:hover:not(:disabled) { background: var(--button-warning-bg-hover); - transform: translateY(-1px); } /* Button Sizes */