feat:weiter chatbot implementiert
This commit is contained in:
parent
eb280dbae1
commit
b0826a3f9a
9 changed files with 664 additions and 68 deletions
|
|
@ -73,6 +73,8 @@ export async function startChatbotStreamApi(
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
// Prepare request body
|
// Prepare request body
|
||||||
|
console.log('[startChatbotStreamApi] requestBody received:', JSON.stringify(requestBody, null, 2));
|
||||||
|
|
||||||
const body: any = {
|
const body: any = {
|
||||||
prompt: requestBody.prompt,
|
prompt: requestBody.prompt,
|
||||||
...(requestBody.listFileId && requestBody.listFileId.length > 0 && { listFileId: requestBody.listFileId }),
|
...(requestBody.listFileId && requestBody.listFileId.length > 0 && { listFileId: requestBody.listFileId }),
|
||||||
|
|
@ -80,6 +82,8 @@ export async function startChatbotStreamApi(
|
||||||
...(requestBody.metadata && { metadata: requestBody.metadata })
|
...(requestBody.metadata && { metadata: requestBody.metadata })
|
||||||
};
|
};
|
||||||
|
|
||||||
|
console.log('[startChatbotStreamApi] body being sent:', JSON.stringify(body, null, 2));
|
||||||
|
|
||||||
// Add workflowId to query params if provided
|
// Add workflowId to query params if provided
|
||||||
const url = requestBody.workflowId
|
const url = requestBody.workflowId
|
||||||
? `/api/chatbot/start/stream?workflowId=${encodeURIComponent(requestBody.workflowId)}`
|
? `/api/chatbot/start/stream?workflowId=${encodeURIComponent(requestBody.workflowId)}`
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,6 @@
|
||||||
|
|
||||||
.actionButton:hover {
|
.actionButton:hover {
|
||||||
background: var(--color-secondary-hover);
|
background: var(--color-secondary-hover);
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.actionButton:disabled {
|
.actionButton:disabled {
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ export function FormGeneratorControls({
|
||||||
filterFocused,
|
filterFocused,
|
||||||
onFilterFocus,
|
onFilterFocus,
|
||||||
selectedCount,
|
selectedCount,
|
||||||
displayData: _displayData,
|
displayData,
|
||||||
onDeleteSingle,
|
onDeleteSingle,
|
||||||
onDeleteMultiple,
|
onDeleteMultiple,
|
||||||
onRefresh,
|
onRefresh,
|
||||||
|
|
@ -76,6 +76,9 @@ export function FormGeneratorControls({
|
||||||
}: FormGeneratorControlsProps) {
|
}: FormGeneratorControlsProps) {
|
||||||
const { t } = useLanguage();
|
const { t } = useLanguage();
|
||||||
|
|
||||||
|
// Check if all items are selected
|
||||||
|
const allItemsSelected = selectedCount > 0 && displayData.length > 0 && selectedCount === displayData.length;
|
||||||
|
|
||||||
// Filter fields that are filterable
|
// Filter fields that are filterable
|
||||||
const filterableFields = fields.filter(field => {
|
const filterableFields = fields.filter(field => {
|
||||||
if (field.type === 'readonly') return false;
|
if (field.type === 'readonly') return false;
|
||||||
|
|
@ -159,7 +162,8 @@ export function FormGeneratorControls({
|
||||||
{/* Delete Controls - Show when items are selected */}
|
{/* Delete Controls - Show when items are selected */}
|
||||||
{selectable && selectedCount > 0 && (
|
{selectable && selectedCount > 0 && (
|
||||||
<div className={styles.deleteControlsIntegrated}>
|
<div className={styles.deleteControlsIntegrated}>
|
||||||
{selectedCount === 1 && onDeleteSingle && (
|
{/* Show delete single only if exactly 1 item selected AND not all items */}
|
||||||
|
{selectedCount === 1 && !allItemsSelected && onDeleteSingle && (
|
||||||
<Button
|
<Button
|
||||||
onClick={onDeleteSingle}
|
onClick={onDeleteSingle}
|
||||||
variant="primary"
|
variant="primary"
|
||||||
|
|
@ -169,14 +173,15 @@ export function FormGeneratorControls({
|
||||||
{t('formgen.delete.single', 'Delete')}
|
{t('formgen.delete.single', 'Delete')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{selectedCount > 1 && onDeleteMultiple && (
|
{/* Show delete multiple if more than 1 selected OR all items are selected */}
|
||||||
|
{(selectedCount > 1 || allItemsSelected) && onDeleteMultiple && (
|
||||||
<Button
|
<Button
|
||||||
onClick={onDeleteMultiple}
|
onClick={onDeleteMultiple}
|
||||||
variant="primary"
|
variant="primary"
|
||||||
size="sm"
|
size="sm"
|
||||||
icon={FaTrash}
|
icon={FaTrash}
|
||||||
>
|
>
|
||||||
{selectedCount === displayData.length && displayData.length > 0
|
{allItemsSelected
|
||||||
? t('formgen.delete.all', `Delete all ${selectedCount} items`).replace('{count}', selectedCount.toString())
|
? t('formgen.delete.all', `Delete all ${selectedCount} items`).replace('{count}', selectedCount.toString())
|
||||||
: t('formgen.delete.multiple', `Delete ${selectedCount} selected items`).replace('{count}', selectedCount.toString())}
|
: t('formgen.delete.multiple', `Delete ${selectedCount} selected items`).replace('{count}', selectedCount.toString())}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@
|
||||||
max-height: none;
|
max-height: none;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding-right: 0.5rem;
|
padding-right: 0.5rem;
|
||||||
|
padding-left: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.emptyList {
|
.emptyList {
|
||||||
|
|
@ -107,6 +108,65 @@
|
||||||
.headerButtonWrapper {
|
.headerButtonWrapper {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
flex-shrink: 0;
|
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 {
|
.listCount {
|
||||||
|
|
@ -165,7 +225,7 @@
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 0.875rem 0.75rem;
|
padding: 0.875rem 1rem;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
border-top: 1px solid var(--color-medium-gray);
|
border-top: 1px solid var(--color-medium-gray);
|
||||||
|
|
@ -248,6 +308,17 @@
|
||||||
gap: 0.375rem;
|
gap: 0.375rem;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
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 {
|
.itemField {
|
||||||
|
|
@ -262,27 +333,69 @@
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.itemField:nth-child(2),
|
.itemField:first-child .fieldValue {
|
||||||
.itemField:nth-child(3) {
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadataFields .itemField {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-top: 0.375rem;
|
margin: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.itemField:nth-child(2) {
|
.metadataFields .itemField .fieldValue {
|
||||||
margin-right: 0.625rem;
|
font-size: 0.75rem;
|
||||||
}
|
font-weight: 400;
|
||||||
|
color: var(--color-text-secondary, #666);
|
||||||
.itemField:nth-child(3) {
|
opacity: 0.8;
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.itemField:nth-child(2) .fieldValue,
|
|
||||||
.itemField:nth-child(3) .fieldValue {
|
|
||||||
display: inline;
|
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 {
|
.fieldLabel {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
@ -496,11 +609,9 @@
|
||||||
@keyframes slideInFromTop {
|
@keyframes slideInFromTop {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(-10px);
|
|
||||||
}
|
}
|
||||||
to {
|
to {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translateY(0);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import React, { useState, useMemo, useRef, useEffect } from 'react';
|
import React, { useState, useMemo, useRef, useEffect } from 'react';
|
||||||
import { useLanguage } from '../../../providers/language/LanguageContext';
|
import { useLanguage } from '../../../providers/language/LanguageContext';
|
||||||
import styles from './FormGeneratorList.module.css';
|
import styles from './FormGeneratorList.module.css';
|
||||||
|
import actionButtonStyles from '../ActionButtons/ActionButton.module.css';
|
||||||
import {
|
import {
|
||||||
EditActionButton,
|
EditActionButton,
|
||||||
DeleteActionButton,
|
DeleteActionButton,
|
||||||
|
|
@ -13,6 +14,7 @@ import {
|
||||||
import { formatUnixTimestamp } from '../../../utils/time';
|
import { formatUnixTimestamp } from '../../../utils/time';
|
||||||
import TextField from '../../UiComponents/TextField/TextField';
|
import TextField from '../../UiComponents/TextField/TextField';
|
||||||
import { FormGeneratorControls } from '../FormGeneratorControls';
|
import { FormGeneratorControls } from '../FormGeneratorControls';
|
||||||
|
import { IoIosTrash, IoIosCheckmark, IoIosClose } from "react-icons/io";
|
||||||
import {
|
import {
|
||||||
isSelectType,
|
isSelectType,
|
||||||
isCheckboxType,
|
isCheckboxType,
|
||||||
|
|
@ -185,6 +187,8 @@ export function FormGeneratorList<T extends Record<string, any>>({
|
||||||
const [selectedItems, setSelectedItems] = useState<Set<number>>(new Set());
|
const [selectedItems, setSelectedItems] = useState<Set<number>>(new Set());
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const [currentPageSize, setCurrentPageSize] = useState(pageSize);
|
const [currentPageSize, setCurrentPageSize] = useState(pageSize);
|
||||||
|
const [isConfirmingDelete, setIsConfirmingDelete] = useState(false);
|
||||||
|
const [isDeleting, setIsDeleting] = useState(false);
|
||||||
|
|
||||||
// Check if backend pagination is supported
|
// Check if backend pagination is supported
|
||||||
const supportsBackendPagination = hookData?.refetch && typeof hookData.refetch === 'function';
|
const supportsBackendPagination = hookData?.refetch && typeof hookData.refetch === 'function';
|
||||||
|
|
@ -244,6 +248,12 @@ export function FormGeneratorList<T extends Record<string, any>>({
|
||||||
// Data is already filtered, sorted, and paginated by the backend
|
// Data is already filtered, sorted, and paginated by the backend
|
||||||
const displayData = data;
|
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
|
// Get pagination info from backend
|
||||||
const totalPages = useMemo(() => {
|
const totalPages = useMemo(() => {
|
||||||
if (!supportsBackendPagination || !hookData?.pagination) {
|
if (!supportsBackendPagination || !hookData?.pagination) {
|
||||||
|
|
@ -324,38 +334,164 @@ export function FormGeneratorList<T extends Record<string, any>>({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle delete single item
|
// Handle delete multiple items click (show confirmation)
|
||||||
const handleDeleteSingle = (row: T, index: number) => {
|
const handleDeleteMultipleClick = () => {
|
||||||
if (onDelete) {
|
if (selectedItems.size === 0 || isDeleting) return;
|
||||||
onDelete(row);
|
setIsConfirmingDelete(true);
|
||||||
if (selectedItems.has(index)) {
|
};
|
||||||
const newSelected = new Set(selectedItems);
|
|
||||||
newSelected.delete(index);
|
// Handle confirm delete
|
||||||
setSelectedItems(newSelected);
|
const handleConfirmDelete = async () => {
|
||||||
if (onItemSelect) {
|
if (selectedItems.size === 0) {
|
||||||
const selectedData = Array.from(newSelected).map(i => displayData[i]);
|
setIsConfirmingDelete(false);
|
||||||
onItemSelect(selectedData);
|
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
|
// Handle cancel delete
|
||||||
const handleDeleteMultiple = () => {
|
const handleCancelDelete = () => {
|
||||||
if (onDeleteMultiple && selectedItems.size > 0) {
|
setIsConfirmingDelete(false);
|
||||||
const selectedData = Array.from(selectedItems).map(i => displayData[i]);
|
|
||||||
onDeleteMultiple(selectedData);
|
|
||||||
setSelectedItems(new Set());
|
|
||||||
onItemSelect?.([]);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 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
|
// Handle page size change
|
||||||
const handlePageSizeChange = (newPageSize: number) => {
|
const handlePageSizeChange = (newPageSize: number) => {
|
||||||
setCurrentPageSize(newPageSize);
|
setCurrentPageSize(newPageSize);
|
||||||
setCurrentPage(1);
|
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
|
// Format field value
|
||||||
const formatFieldValue = (value: any, field: FieldConfig, row: T) => {
|
const formatFieldValue = (value: any, field: FieldConfig, row: T) => {
|
||||||
if (field.formatter) {
|
if (field.formatter) {
|
||||||
|
|
@ -459,10 +595,27 @@ export function FormGeneratorList<T extends Record<string, any>>({
|
||||||
|
|
||||||
// Render field input
|
// Render field input
|
||||||
const renderFieldInput = (field: FieldConfig, value: any, row: T, _index: number) => {
|
const renderFieldInput = (field: FieldConfig, value: any, row: T, _index: number) => {
|
||||||
|
const isStatusField = field.key.toLowerCase().includes('status');
|
||||||
|
|
||||||
if (field.type === 'readonly' || !field.editable) {
|
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 (
|
||||||
|
<div className={styles.fieldValue} key={field.key}>
|
||||||
|
<span className={`${styles.statusBadge} ${statusClass}`}>
|
||||||
|
{formattedValue}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.fieldValue} key={field.key}>
|
<div className={styles.fieldValue} key={field.key}>
|
||||||
{formatFieldValue(value, field, row)}
|
{formattedValue}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -515,7 +668,7 @@ export function FormGeneratorList<T extends Record<string, any>>({
|
||||||
return (
|
return (
|
||||||
<div className={`${styles.formGeneratorList} ${className}`}>
|
<div className={`${styles.formGeneratorList} ${className}`}>
|
||||||
|
|
||||||
{(searchable || filterable || (selectable && selectedItems.size > 0)) && (
|
{(searchable || filterable) && selectedItems.size === 0 && (
|
||||||
<FormGeneratorControls
|
<FormGeneratorControls
|
||||||
fields={detectedFields}
|
fields={detectedFields}
|
||||||
searchTerm={searchTerm}
|
searchTerm={searchTerm}
|
||||||
|
|
@ -528,12 +681,8 @@ export function FormGeneratorList<T extends Record<string, any>>({
|
||||||
onFilterFocus={handleFilterFocus}
|
onFilterFocus={handleFilterFocus}
|
||||||
selectedCount={selectedItems.size}
|
selectedCount={selectedItems.size}
|
||||||
displayData={displayData}
|
displayData={displayData}
|
||||||
onDeleteSingle={selectedItems.size === 1 && onDelete ? () => {
|
onDeleteSingle={undefined}
|
||||||
const selectedIndex = Array.from(selectedItems)[0];
|
onDeleteMultiple={undefined}
|
||||||
const selectedRow = displayData[selectedIndex];
|
|
||||||
handleDeleteSingle(selectedRow, selectedIndex);
|
|
||||||
} : undefined}
|
|
||||||
onDeleteMultiple={(selectedItems.size > 1 || (selectedItems.size === displayData.length && displayData.length > 0)) && onDeleteMultiple ? handleDeleteMultiple : undefined}
|
|
||||||
onRefresh={onRefresh}
|
onRefresh={onRefresh}
|
||||||
searchable={searchable}
|
searchable={searchable}
|
||||||
filterable={filterable}
|
filterable={filterable}
|
||||||
|
|
@ -573,6 +722,47 @@ export function FormGeneratorList<T extends Record<string, any>>({
|
||||||
{title && data.length > 0 && (
|
{title && data.length > 0 && (
|
||||||
<span className={styles.listCount}>({data.length})</span>
|
<span className={styles.listCount}>({data.length})</span>
|
||||||
)}
|
)}
|
||||||
|
{hasSelectedItems && (onDeleteMultiple || onDelete) && (
|
||||||
|
<div className={styles.headerDeleteButtonContainer}>
|
||||||
|
{isConfirmingDelete ? (
|
||||||
|
<div className={actionButtonStyles.deleteConfirmButtons}>
|
||||||
|
<button
|
||||||
|
onClick={handleConfirmDelete}
|
||||||
|
className={`${actionButtonStyles.actionButton} ${actionButtonStyles.confirmButton}`}
|
||||||
|
title={t('formgen.delete.confirm', 'Confirm delete')}
|
||||||
|
disabled={isDeleting}
|
||||||
|
>
|
||||||
|
<span className={actionButtonStyles.actionIcon}>
|
||||||
|
<IoIosCheckmark />
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleCancelDelete}
|
||||||
|
className={`${actionButtonStyles.actionButton} ${actionButtonStyles.cancelButton}`}
|
||||||
|
title={t('formgen.delete.cancel', 'Cancel delete')}
|
||||||
|
disabled={isDeleting}
|
||||||
|
>
|
||||||
|
<span className={actionButtonStyles.actionIcon}>
|
||||||
|
<IoIosClose />
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
onClick={handleDeleteMultipleClick}
|
||||||
|
className={`${actionButtonStyles.actionButton} ${actionButtonStyles.delete} ${styles.headerDeleteButton} ${isDeleting ? actionButtonStyles.loading : ''}`}
|
||||||
|
title={allItemsSelected
|
||||||
|
? t('formgen.delete.all', `Delete all ${selectedItems.size} items`)
|
||||||
|
: t('formgen.delete.multiple', `Delete ${selectedItems.size} selected items`)}
|
||||||
|
disabled={isDeleting}
|
||||||
|
>
|
||||||
|
<span className={actionButtonStyles.actionIcon}>
|
||||||
|
<IoIosTrash />
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{headerButton && (
|
{headerButton && (
|
||||||
<div className={styles.headerButtonWrapper}>
|
<div className={styles.headerButtonWrapper}>
|
||||||
{headerButton}
|
{headerButton}
|
||||||
|
|
@ -713,17 +903,61 @@ export function FormGeneratorList<T extends Record<string, any>>({
|
||||||
|
|
||||||
{/* Fields */}
|
{/* Fields */}
|
||||||
<div className={styles.itemFields}>
|
<div className={styles.itemFields}>
|
||||||
{detectedFields.map(field => {
|
{detectedFields.map((field, fieldIndex) => {
|
||||||
const cellValue = row[field.key];
|
const cellValue = row[field.key];
|
||||||
const customClassName = field.cellClassName ? field.cellClassName(cellValue, row) : '';
|
const customClassName = field.cellClassName ? field.cellClassName(cellValue, row) : '';
|
||||||
|
|
||||||
|
// First field (name) - render normally
|
||||||
|
if (fieldIndex === 0) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={field.key}
|
||||||
|
className={`${styles.itemField} ${customClassName}`}
|
||||||
|
>
|
||||||
|
<label className={styles.fieldLabel}>{field.label}</label>
|
||||||
|
{renderFieldInput(field, cellValue, row, fieldIndex)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 (
|
||||||
|
<div key={`metadata-${field.key}`} className={styles.metadataFields}>
|
||||||
|
<div
|
||||||
|
className={`${styles.itemField} ${customClassName}`}
|
||||||
|
>
|
||||||
|
<label className={styles.fieldLabel}>{field.label}</label>
|
||||||
|
{renderFieldInput(field, cellValue, row, fieldIndex)}
|
||||||
|
</div>
|
||||||
|
{nextField && (
|
||||||
|
<div
|
||||||
|
className={`${styles.itemField} ${nextField.cellClassName ? nextField.cellClassName(nextFieldValue, row) : ''}`}
|
||||||
|
>
|
||||||
|
<label className={styles.fieldLabel}>{nextField.label}</label>
|
||||||
|
{renderFieldInput(nextField, nextFieldValue, row, 2)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 (
|
return (
|
||||||
<div
|
<div
|
||||||
key={field.key}
|
key={field.key}
|
||||||
className={`${styles.itemField} ${customClassName}`}
|
className={`${styles.itemField} ${customClassName}`}
|
||||||
>
|
>
|
||||||
<label className={styles.fieldLabel}>{field.label}</label>
|
<label className={styles.fieldLabel}>{field.label}</label>
|
||||||
{renderFieldInput(field, cellValue, row)}
|
{renderFieldInput(field, cellValue, row, fieldIndex)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
|
||||||
|
|
@ -1016,9 +1016,12 @@ const PageRenderer: React.FC<PageRendererProps> = ({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Check if we have file management
|
// Check if we have file management (dashboard style with workflowFiles)
|
||||||
const hasFileManagement = !!(hookData.handleFileUpload && hookData.workflowFiles !== undefined);
|
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
|
// 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)
|
// Show prompt selector if user has permission to view/read prompts (even if no prompts exist yet)
|
||||||
const showPromptSelector = hookData.promptPermission &&
|
const showPromptSelector = hookData.promptPermission &&
|
||||||
|
|
@ -1291,6 +1294,130 @@ const PageRenderer: React.FC<PageRendererProps> = ({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Chatbot file upload layout (simpler than dashboard)
|
||||||
|
if (hasChatbotFileUpload) {
|
||||||
|
const uploadedFiles = hookData.uploadedFiles || [];
|
||||||
|
return (
|
||||||
|
<div key={content.id} style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: '0.75rem',
|
||||||
|
margin: '1.5rem 0',
|
||||||
|
width: '100%',
|
||||||
|
flexShrink: 0
|
||||||
|
}}>
|
||||||
|
{/* Input and buttons row */}
|
||||||
|
<div style={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: '8px',
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
width: '100%'
|
||||||
|
}}>
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
|
<TextField
|
||||||
|
value={hookData.inputValue || ''}
|
||||||
|
onChange={hookData.onInputChange}
|
||||||
|
placeholder={resolveLanguageText(config.placeholder, t)}
|
||||||
|
size={config.textFieldSize || 'md'}
|
||||||
|
disabled={hookData.isSubmitting || false}
|
||||||
|
{...({ onKeyDown: handleKeyDown } as any)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div style={{ flexShrink: 0, display: 'flex', gap: '8px' }}>
|
||||||
|
<UploadButton
|
||||||
|
onUpload={hookData.handleFileUpload ? async (file: File) => {
|
||||||
|
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
|
||||||
|
</UploadButton>
|
||||||
|
<Button
|
||||||
|
onClick={() => hookData.handleSubmit?.()}
|
||||||
|
loading={hookData.isSubmitting || false}
|
||||||
|
disabled={buttonDisabled}
|
||||||
|
variant={buttonVariant}
|
||||||
|
size={config.buttonSize || 'md'}
|
||||||
|
icon={buttonIcon}
|
||||||
|
>
|
||||||
|
{resolveLanguageText(buttonLabel, t)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Pending files display */}
|
||||||
|
{uploadedFiles.length > 0 && (
|
||||||
|
<div style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
gap: '6px',
|
||||||
|
padding: '8px',
|
||||||
|
backgroundColor: '#f5f5f5',
|
||||||
|
borderRadius: '4px',
|
||||||
|
fontSize: '0.875rem'
|
||||||
|
}}>
|
||||||
|
{uploadedFiles.map((file: { fileId: string; fileName: string }) => (
|
||||||
|
<div
|
||||||
|
key={file.fileId}
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '6px',
|
||||||
|
padding: '4px 8px',
|
||||||
|
backgroundColor: 'white',
|
||||||
|
border: '1px solid #e0e0e0',
|
||||||
|
borderRadius: '4px',
|
||||||
|
fontSize: '0.8rem'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>📎</span>
|
||||||
|
<span style={{ maxWidth: '150px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||||
|
{file.fileName}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={() => hookData.handleFileRemove?.(file.fileId)}
|
||||||
|
disabled={hookData.isSubmitting || false}
|
||||||
|
style={{
|
||||||
|
background: 'none',
|
||||||
|
border: 'none',
|
||||||
|
cursor: hookData.isSubmitting ? 'not-allowed' : 'pointer',
|
||||||
|
padding: '0',
|
||||||
|
marginLeft: '4px',
|
||||||
|
fontSize: '1rem',
|
||||||
|
lineHeight: '1',
|
||||||
|
opacity: hookData.isSubmitting ? 0.5 : 1
|
||||||
|
}}
|
||||||
|
title="Remove file"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Upload error display */}
|
||||||
|
{hookData.uploadError && (
|
||||||
|
<div style={{
|
||||||
|
padding: '8px 12px',
|
||||||
|
backgroundColor: '#ffebee',
|
||||||
|
color: '#c62828',
|
||||||
|
borderRadius: '4px',
|
||||||
|
fontSize: '0.875rem'
|
||||||
|
}}>
|
||||||
|
{hookData.uploadError}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Default layout without files (backward compatible)
|
// Default layout without files (backward compatible)
|
||||||
return (
|
return (
|
||||||
<div key={content.id} style={{
|
<div key={content.id} style={{
|
||||||
|
|
@ -1682,19 +1809,17 @@ const PageRenderer: React.FC<PageRendererProps> = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// If the page has drag drop config and hook data with handleUpload, integrate them
|
// If the page has drag drop config and hook data with handleUpload or handleFileUpload, integrate them
|
||||||
if (hookData?.handleUpload) {
|
const uploadHandler = hookData?.handleUpload || hookData?.handleFileUpload;
|
||||||
|
if (uploadHandler) {
|
||||||
return {
|
return {
|
||||||
...pageData.dragDropConfig,
|
...pageData.dragDropConfig,
|
||||||
onDrop: async (files: File[]) => {
|
onDrop: async (files: File[]) => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Process each file through the hook's handleUpload function
|
// Process each file through the hook's upload function
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
if (hookData.handleUpload) {
|
if (uploadHandler) {
|
||||||
|
await uploadHandler(file);
|
||||||
await hookData.handleUpload(file);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,15 @@ export const chatbotPageData: GenericPageData = {
|
||||||
preload: true,
|
preload: true,
|
||||||
moduleEnabled: 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
|
// Lifecycle hooks
|
||||||
onActivate: async () => {
|
onActivate: async () => {
|
||||||
if (import.meta.env.DEV) console.log('Chatbot activated - state preserved');
|
if (import.meta.env.DEV) console.log('Chatbot activated - state preserved');
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { useState, useCallback, useRef, useMemo, useEffect } from 'react';
|
import { useState, useCallback, useRef, useMemo, useEffect } from 'react';
|
||||||
import { useApiRequest } from './useApi';
|
import { useApiRequest } from './useApi';
|
||||||
|
import api from '../api';
|
||||||
import {
|
import {
|
||||||
startChatbotStreamApi,
|
startChatbotStreamApi,
|
||||||
stopChatbotApi,
|
stopChatbotApi,
|
||||||
|
|
@ -32,6 +33,13 @@ export function useChatbot() {
|
||||||
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
|
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// File upload state
|
||||||
|
const [pendingFileIds, setPendingFileIds] = useState<string[]>([]);
|
||||||
|
const pendingFileIdsRef = useRef<string[]>([]); // Ref to avoid closure issues
|
||||||
|
const [uploadingFile, setUploadingFile] = useState<boolean>(false);
|
||||||
|
const [uploadError, setUploadError] = useState<string | null>(null);
|
||||||
|
const [uploadedFiles, setUploadedFiles] = useState<Array<{ fileId: string; fileName: string }>>([]);
|
||||||
|
|
||||||
// Chat history state
|
// Chat history state
|
||||||
const [threads, setThreads] = useState<ChatbotWorkflow[]>([]);
|
const [threads, setThreads] = useState<ChatbotWorkflow[]>([]);
|
||||||
const [selectedThreadId, setSelectedThreadId] = useState<string | null>(null);
|
const [selectedThreadId, setSelectedThreadId] = useState<string | null>(null);
|
||||||
|
|
@ -340,6 +348,73 @@ export function useChatbot() {
|
||||||
setInputValue(value);
|
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
|
// Stop chatbot workflow
|
||||||
const stopChatbot = useCallback(async () => {
|
const stopChatbot = useCallback(async () => {
|
||||||
if (!workflowId || !isRunning) {
|
if (!workflowId || !isRunning) {
|
||||||
|
|
@ -398,13 +473,32 @@ export function useChatbot() {
|
||||||
const abortController = new AbortController();
|
const abortController = new AbortController();
|
||||||
streamAbortControllerRef.current = 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 = {
|
const requestBody: StartChatbotRequest = {
|
||||||
prompt: trimmedInput,
|
prompt: trimmedInput,
|
||||||
userLanguage: 'en',
|
userLanguage: 'en',
|
||||||
...(workflowId && { workflowId })
|
...(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
|
// Track if workflow was created in this request
|
||||||
let workflowCreated = false;
|
let workflowCreated = false;
|
||||||
|
|
||||||
|
|
@ -452,6 +546,10 @@ export function useChatbot() {
|
||||||
if (!abortController.signal.aborted) {
|
if (!abortController.signal.aborted) {
|
||||||
setIsRunning(false);
|
setIsRunning(false);
|
||||||
setInputValue(''); // Clear input on completion
|
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
|
// Clear thinking message on completion if no final message was received
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
clearThinkingMessage();
|
clearThinkingMessage();
|
||||||
|
|
@ -474,7 +572,7 @@ export function useChatbot() {
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
streamAbortControllerRef.current = null;
|
streamAbortControllerRef.current = null;
|
||||||
}
|
}
|
||||||
}, [inputValue, workflowId, isRunning, isSubmitting, stopChatbot, processChatDataItem, clearThinkingMessage, loadThreads]);
|
}, [inputValue, workflowId, isRunning, isSubmitting, stopChatbot, processChatDataItem, clearThinkingMessage, loadThreads, pendingFileIds]);
|
||||||
|
|
||||||
// Delete a chatbot workflow
|
// Delete a chatbot workflow
|
||||||
const handleDeleteThread = useCallback(async (workflowIdToDelete: string): Promise<boolean> => {
|
const handleDeleteThread = useCallback(async (workflowIdToDelete: string): Promise<boolean> => {
|
||||||
|
|
@ -534,6 +632,9 @@ export function useChatbot() {
|
||||||
setSelectedThreadId(null);
|
setSelectedThreadId(null);
|
||||||
setError(null);
|
setError(null);
|
||||||
setInputValue('');
|
setInputValue('');
|
||||||
|
setPendingFileIds([]);
|
||||||
|
pendingFileIdsRef.current = [];
|
||||||
|
setUploadedFiles([]);
|
||||||
thinkingLogsRef.current = [];
|
thinkingLogsRef.current = [];
|
||||||
thinkingMessageIdRef.current = null;
|
thinkingMessageIdRef.current = null;
|
||||||
clearProcessedMessages();
|
clearProcessedMessages();
|
||||||
|
|
@ -554,6 +655,9 @@ export function useChatbot() {
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
setError(null);
|
setError(null);
|
||||||
setInputValue('');
|
setInputValue('');
|
||||||
|
setPendingFileIds([]);
|
||||||
|
pendingFileIdsRef.current = [];
|
||||||
|
setUploadedFiles([]);
|
||||||
thinkingLogsRef.current = [];
|
thinkingLogsRef.current = [];
|
||||||
thinkingMessageIdRef.current = null;
|
thinkingMessageIdRef.current = null;
|
||||||
clearProcessedMessages();
|
clearProcessedMessages();
|
||||||
|
|
@ -613,7 +717,16 @@ export function useChatbot() {
|
||||||
stopChatbot,
|
stopChatbot,
|
||||||
resetChatbot,
|
resetChatbot,
|
||||||
startNewChat,
|
startNewChat,
|
||||||
cleanup
|
cleanup,
|
||||||
|
|
||||||
|
// File upload interface
|
||||||
|
handleFileUpload,
|
||||||
|
handleUpload: handleFileUpload, // Alias for compatibility with DragDropOverlay
|
||||||
|
handleFileRemove,
|
||||||
|
pendingFileIds,
|
||||||
|
uploadedFiles,
|
||||||
|
uploadingFile,
|
||||||
|
uploadError
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -96,7 +96,6 @@
|
||||||
|
|
||||||
.buttonPrimary:hover:not(:disabled) {
|
.buttonPrimary:hover:not(:disabled) {
|
||||||
background: var(--button-primary-bg-hover);
|
background: var(--button-primary-bg-hover);
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.buttonSecondary {
|
.buttonSecondary {
|
||||||
|
|
@ -105,8 +104,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.buttonSecondary:hover:not(:disabled) {
|
.buttonSecondary:hover:not(:disabled) {
|
||||||
background: var(--button-secondary-bg-hover);
|
background: var(--color-secondary-hover);
|
||||||
transform: translateY(-1px);
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.buttonDanger {
|
.buttonDanger {
|
||||||
|
|
@ -116,7 +115,6 @@
|
||||||
|
|
||||||
.buttonDanger:hover:not(:disabled) {
|
.buttonDanger:hover:not(:disabled) {
|
||||||
background: var(--button-danger-bg-hover);
|
background: var(--button-danger-bg-hover);
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.buttonSuccess {
|
.buttonSuccess {
|
||||||
|
|
@ -126,7 +124,6 @@
|
||||||
|
|
||||||
.buttonSuccess:hover:not(:disabled) {
|
.buttonSuccess:hover:not(:disabled) {
|
||||||
background: var(--button-success-bg-hover);
|
background: var(--button-success-bg-hover);
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.buttonWarning {
|
.buttonWarning {
|
||||||
|
|
@ -136,7 +133,6 @@
|
||||||
|
|
||||||
.buttonWarning:hover:not(:disabled) {
|
.buttonWarning:hover:not(:disabled) {
|
||||||
background: var(--button-warning-bg-hover);
|
background: var(--button-warning-bg-hover);
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Button Sizes */
|
/* Button Sizes */
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue