import { Textarea, ShortTextInput } from '@/components' import { SendButton } from '@/components/SendButton' import { CommandData } from '@/features/commands' import { Attachment, BotContext, InputSubmitContent } from '@/types' import { isMobile } from '@/utils/isMobileSignal' import type { TextInputBlock } from '@indite.io/schemas' import { For, Match, Show, Switch, createSignal, onCleanup, onMount, } from 'solid-js' import { defaultTextInputOptions } from '@indite.io/schemas/features/blocks/inputs/text/constants' import clsx from 'clsx' import { TextInputAddFileButton } from '@/components/TextInputAddFileButton' import { SelectedFile } from '../../fileUpload/components/SelectedFile' import { sanitizeNewFile } from '../../fileUpload/helpers/sanitizeSelectedFiles' import { getRuntimeVariable } from '@indite.io/env/getRuntimeVariable' import { toaster } from '@/utils/toaster' import { isDefined } from '@indite.io/lib' import { uploadFiles } from '../../fileUpload/helpers/uploadFiles' import { guessApiHost } from '@/utils/guessApiHost' import { VoiceRecorder } from './VoiceRecorder' import { Button } from '@/components/Button' import { MicrophoneIcon } from '@/components/icons/MicrophoneIcon' type Props = { block: TextInputBlock defaultValue?: string context: BotContext onSubmit: (value: InputSubmitContent) => void } export const TextInput = (props: Props) => { const [inputValue, setInputValue] = createSignal(props.defaultValue ?? '') const [selectedFiles, setSelectedFiles] = createSignal([]) const [uploadProgress, setUploadProgress] = createSignal< { fileIndex: number; progress: number } | undefined >(undefined) const [isDraggingOver, setIsDraggingOver] = createSignal(false) const [recordingStatus, setRecordingStatus] = createSignal< 'started' | 'asking' | 'stopped' >('stopped') let inputRef: HTMLInputElement | HTMLTextAreaElement | undefined let mediaRecorder: MediaRecorder | undefined let recordedChunks: Blob[] = [] const handleInput = (inputValue: string) => setInputValue(inputValue) const checkIfInputIsValid = () => inputRef?.value !== '' && inputRef?.reportValidity() const submit = async () => { if (recordingStatus() === 'started' && mediaRecorder) { mediaRecorder.stop() return } if (checkIfInputIsValid()) { let attachments: Attachment[] | undefined if (selectedFiles().length > 0) { setUploadProgress(undefined) const urls = await uploadFiles({ apiHost: props.context.apiHost ?? guessApiHost({ ignoreChatApiUrl: true }), files: selectedFiles().map((file) => ({ file: file, input: { sessionId: props.context.sessionId, fileName: file.name, }, })), onUploadProgress: setUploadProgress, }) attachments = urls?.filter(isDefined) } props.onSubmit({ type: 'text', value: inputRef?.value ?? inputValue(), attachments, }) } else inputRef?.focus() setInputValue('') } const submitWhenEnter = (e: KeyboardEvent) => { if (props.block.options?.isLong) return if (e.key === 'Enter') submit() } const submitIfCtrlEnter = (e: KeyboardEvent) => { if (!props.block.options?.isLong) return if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) submit() } onMount(() => { if (!isMobile() && inputRef) inputRef.focus({ preventScroll: true, }) window.addEventListener('message', processIncomingEvent) }) onCleanup(() => { window.removeEventListener('message', processIncomingEvent) }) const processIncomingEvent = (event: MessageEvent) => { const { data } = event if (!data.isFromBot) return if (data.command === 'setInputValue') setInputValue(data.value) } const handleDragOver = (e: DragEvent) => { e.preventDefault() setIsDraggingOver(true) } const handleDragLeave = () => setIsDraggingOver(false) const handleDropFile = (e: DragEvent) => { e.preventDefault() e.stopPropagation() if (!e.dataTransfer?.files) return onNewFiles(e.dataTransfer.files) } const onNewFiles = (files: FileList) => { const newFiles = Array.from(files) .map((file) => sanitizeNewFile({ existingFiles: selectedFiles(), newFile: file, params: { sizeLimit: getRuntimeVariable( 'NEXT_PUBLIC_BOT_FILE_UPLOAD_MAX_SIZE' ), }, onError: ({ description, title }) => { toaster.create({ description, title, }) }, }) ) .filter(isDefined) if (newFiles.length === 0) return setSelectedFiles((selectedFiles) => [...newFiles, ...selectedFiles]) } const removeSelectedFile = (index: number) => { setSelectedFiles((selectedFiles) => selectedFiles.filter((_, i) => i !== index) ) } const recordVoice = () => { setRecordingStatus('asking') } const handleRecordingConfirmed = (stream: MediaStream) => { mediaRecorder = new MediaRecorder(stream) mediaRecorder.ondataavailable = (event) => { if (event.data.size === 0) return recordedChunks.push(event.data) } mediaRecorder.onstop = async () => { if (recordingStatus() !== 'started' || recordedChunks.length === 0) return const audioFile = new File( recordedChunks, `rec-${props.block.id}-${Date.now()}.mp3`, { type: 'audio/mp3', } ) setUploadProgress(undefined) const urls = ( await uploadFiles({ apiHost: props.context.apiHost ?? guessApiHost({ ignoreChatApiUrl: true }), files: [ { file: audioFile, input: { sessionId: props.context.sessionId, fileName: audioFile.name, }, }, ], onUploadProgress: setUploadProgress, }) ) .filter(isDefined) .map((url) => url.url) props.onSubmit({ type: 'recording', url: urls[0], }) } mediaRecorder.start() setRecordingStatus('started') } const handleRecordingAbort = () => { mediaRecorder?.stop() setRecordingStatus('stopped') mediaRecorder = undefined recordedChunks = [] } return (
{(file, index) => ( removeSelectedFile(index())} /> )}
{props.block.options?.isLong ? (