ui-nyla/src/components/Dashboard/DashboardChat/DashboardChatAreaInput.tsx

243 lines
7.7 KiB
TypeScript

import React, { useState, useEffect, useRef } from 'react';
import { Prompt } from '../../../hooks/usePrompts';
import FileAttachmentPopup from './FileAttachmentPopup';
import { WorkflowManagerState, WorkflowManagerActions } from './useWorkflowManager';
import { useLanguage } from '../../../contexts/LanguageContext';
import styles from './DashboardChatAreaStyles/DashboardChatAreaInput.module.css';
import sharedStyles from './DashboardChatAreaStyles/DashboardChat.module.css';
interface InputAreaProps {
selectedPrompt?: Prompt | null;
onPromptUsed?: () => void;
workflowState: WorkflowManagerState;
workflowActions: WorkflowManagerActions;
onAttachedFilesChange?: (files: AttachedFile[]) => void;
attachedFiles?: AttachedFile[];
}
interface AttachedFile {
id: number;
name: string;
size: number;
type: string;
fileData?: File;
objectUrl?: string;
}
const InputArea: React.FC<InputAreaProps> = ({
selectedPrompt,
onPromptUsed,
workflowState,
workflowActions,
onAttachedFilesChange,
attachedFiles: externalAttachedFiles = []
}) => {
const { t } = useLanguage();
const [inputValue, setInputValue] = useState('');
const [showFilePopup, setShowFilePopup] = useState(false);
const [isSending, setIsSending] = useState(false);
const [sendError, setSendError] = useState<string | null>(null);
const [isFocused, setIsFocused] = useState(false);
const textareaRef = useRef<HTMLTextAreaElement>(null);
// Always use external attached files from parent component
const currentAttachedFiles = externalAttachedFiles;
// Auto-resize textarea function
const adjustTextareaHeight = () => {
const textarea = textareaRef.current;
if (!textarea) return;
// Reset height to auto to get the actual scroll height
textarea.style.height = 'auto';
// Calculate the height based on content
const scrollHeight = textarea.scrollHeight;
const lineHeight = 1.5 * 14; // 1.5em * 14px font size
const padding = 32; // 16px top + 16px bottom padding
const minHeight = lineHeight * 4 + padding; // 4 rows
const maxHeight = lineHeight * 8 + padding; // 8 rows
// Set height within constraints
const newHeight = Math.min(Math.max(scrollHeight, minHeight), maxHeight);
textarea.style.height = `${newHeight}px`;
};
// Auto-fill input when prompt is selected
useEffect(() => {
if (selectedPrompt) {
setInputValue(selectedPrompt.content);
}
}, [selectedPrompt]);
// Adjust height when input value changes
useEffect(() => {
adjustTextareaHeight();
}, [inputValue]);
// Initial resize on mount
useEffect(() => {
adjustTextareaHeight();
}, []);
const handleSend = async () => {
if (!inputValue.trim() || isSending) return;
setIsSending(true);
setSendError(null);
try {
const fileIds = currentAttachedFiles.map(f => f.id);
let success = false;
if (workflowState.currentWorkflowId) {
// Continue existing workflow
console.log(`➡️ Continuing workflow ${workflowState.currentWorkflowId}`);
success = await workflowActions.continueWorkflow(inputValue, fileIds);
} else {
// Start new workflow
console.log('🚀 Starting new workflow');
const newWorkflowId = await workflowActions.startNewWorkflow(inputValue, fileIds);
success = !!newWorkflowId;
}
if (success) {
setInputValue('');
if (onAttachedFilesChange) {
onAttachedFilesChange([]);
}
if (onPromptUsed) onPromptUsed();
} else {
setSendError('Failed to send message. Please try again.');
}
} catch (error: any) {
console.error('Failed to send message:', error);
setSendError(error.message || 'Failed to send message. Please try again.');
} finally {
setIsSending(false);
}
};
const handleKeyPress = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSend();
}
};
const handleFilesAttached = (files: AttachedFile[]) => {
setShowFilePopup(false);
if (onAttachedFilesChange) {
onAttachedFilesChange(files);
}
};
const isWorkflowActive = workflowState.workflow &&
['running', 'processing', 'started'].includes(workflowState.workflow.status);
// Determine if label should be in focused/moved state
const shouldLabelBeFocused = isFocused || inputValue.trim().length > 0;
// Get placeholder text
const placeholderText = workflowState.currentWorkflowId
? t('chat.input.continue_workflow')
: t('chat.input.enter_message');
return (
<div className={styles.input_area_container}>
{/* Error messages */}
{(sendError || workflowState.error) && (
<div className={styles.error_message}>
{t('chat.input.error_prefix')} {sendError || workflowState.error}
</div>
)}
{/* Show attached files count */}
{currentAttachedFiles.length > 0 && (
<div className={styles.attached_files_count}>
{currentAttachedFiles.length} {currentAttachedFiles.length !== 1 ? t('chat.input.files_attached_plural') : t('chat.input.files_attached')} {t('chat.input.files_attached_label')}
</div>
)}
<div className={styles.input_form_container}>
<div className={styles.floating_label_textarea}>
<label
className={shouldLabelBeFocused ? styles.textarea_label_focused : styles.textarea_label}
>
{placeholderText}
</label>
<textarea
ref={textareaRef}
value={inputValue}
onChange={(e) => {
setInputValue(e.target.value);
// Trigger resize on next frame to ensure DOM is updated
setTimeout(adjustTextareaHeight, 0);
}}
onKeyPress={handleKeyPress}
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
placeholder=""
disabled={isSending || isWorkflowActive}
className={styles.message_textarea}
rows={4}
/>
</div>
<div className={styles.input_actions_row}>
<button
onClick={() => setShowFilePopup(true)}
disabled={isSending || isWorkflowActive}
className={sharedStyles.button_secondary}
>
{t('chat.input.attach_files')}
</button>
<button
onClick={handleSend}
disabled={!inputValue.trim() || isSending || isWorkflowActive}
className={`${sharedStyles.button_primary} ${
(!inputValue.trim() || isSending || isWorkflowActive)
? styles.disabled
: styles.enabled
}`}
>
{isSending ? t('chat.input.sending') :
isWorkflowActive ? t('chat.input.processing') :
workflowState.currentWorkflowId ? t('chat.input.continue') : t('chat.input.send')}
</button>
{workflowState.currentWorkflowId && !isWorkflowActive && (
<button
onClick={() => workflowActions.clearWorkflow()}
className={styles.new_chat_button}
>
{t('chat.input.new_chat')}
</button>
)}
{selectedPrompt && (
<span className={styles.prompt_indicator}>
{t('chat.input.using_prompt')} {selectedPrompt.name}
</span>
)}
</div>
</div>
{/* File Attachment Popup */}
{showFilePopup && (
<FileAttachmentPopup
onClose={() => setShowFilePopup(false)}
onFilesSelected={handleFilesAttached}
currentAttachedFiles={currentAttachedFiles}
/>
)}
</div>
);
};
export default InputArea;