243 lines
7.7 KiB
TypeScript
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;
|