From 213241eae99226f6a99740a8d9a97c38b07edfdf Mon Sep 17 00:00:00 2001 From: Enrico Ros Date: Sun, 12 Jan 2025 00:13:42 -0800 Subject: [PATCH] Web Input: multiball --- .../chat/components/composer/Composer.tsx | 6 +- .../components/composer/WebInputModal.tsx | 156 +++++++++++------- 2 files changed, 98 insertions(+), 64 deletions(-) diff --git a/src/apps/chat/components/composer/Composer.tsx b/src/apps/chat/components/composer/Composer.tsx index ba17f7c9d..e1eef7ddf 100644 --- a/src/apps/chat/components/composer/Composer.tsx +++ b/src/apps/chat/components/composer/Composer.tsx @@ -596,9 +596,11 @@ export function Composer(props: { .catch((error: any) => addSnackbar({ key: 'attach-file-open-fail', message: `Unable to attach the file "${file.name}" (${error?.message || error?.toString() || 'unknown error'})`, type: 'issue' })); }, [attachAppendFile]); - const handleAttachWebUrl = React.useCallback(async (url: string) => attachAppendUrl('input-link', url), [attachAppendUrl]); + const handleAttachWebLinks = React.useCallback(async (urls: string[]) => { + urls.forEach(url => void attachAppendUrl('input-link', url)); + }, [attachAppendUrl]); - const { openWebInputDialog, webInputDialogComponent } = useWebInputModal(handleAttachWebUrl); + const { openWebInputDialog, webInputDialogComponent } = useWebInputModal(handleAttachWebLinks); // Attachments Down diff --git a/src/apps/chat/components/composer/WebInputModal.tsx b/src/apps/chat/components/composer/WebInputModal.tsx index 98dc8e3fe..080e49e23 100644 --- a/src/apps/chat/components/composer/WebInputModal.tsx +++ b/src/apps/chat/components/composer/WebInputModal.tsx @@ -1,7 +1,9 @@ import * as React from 'react'; -import { Controller, useForm } from 'react-hook-form'; +import { Controller, useFieldArray, useForm } from 'react-hook-form'; -import { Box, Button, FormControl, FormHelperText, Input, Typography } from '@mui/joy'; +import { Box, Button, FormControl, FormHelperText, IconButton, Input, Stack, Typography } from '@mui/joy'; +import AddIcon from '@mui/icons-material/Add'; +import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline'; import LanguageRoundedIcon from '@mui/icons-material/LanguageRounded'; import YouTubeIcon from '@mui/icons-material/YouTube'; @@ -12,48 +14,57 @@ import { addSnackbar } from '~/common/components/snackbar/useSnackbarsStore'; import { asValidURL } from '~/common/util/urlUtils'; +// configuration +const MAX_URLS = 5; + + type WebInputModalInputs = { - url: string, + urls: { value: string }[]; } + function WebInputModal(props: { onClose: () => void, - onURLSubmit: (url: string) => void, + onWebLinks: (urls: string[]) => void, }) { // state - const { control: formControl, handleSubmit: formHandleSubmit, formState: { isValid: formIsValid } } = useForm({ - values: { url: '' }, - mode: 'onChange', // validate on change + const { control: formControl, handleSubmit: formHandleSubmit, formState: { isValid: formIsValid, isDirty: formIsDirty } } = useForm({ + values: { urls: [{ value: '' }] }, + // mode: 'onChange', // validate on change }); + const { fields: formFields, append: formFieldsAppend, remove: formFieldsRemove } = useFieldArray({ control: formControl, name: 'urls' }); - // handlers + // derived + const urlFieldCount = formFields.length; - const { onClose, onURLSubmit } = props; - const handleClose = React.useCallback(() => onClose(), [onClose]); + // handlers - const handleSubmit = React.useCallback(({ url }: WebInputModalInputs) => { + const { onClose, onWebLinks } = props; - let normalizedUrl = (url || '').trim(); - // noinspection HttpUrlsUsage - if (!normalizedUrl.startsWith('http://') && !normalizedUrl.startsWith('https://')) - normalizedUrl = 'https://' + normalizedUrl; + const handleClose = React.useCallback(() => onClose(), [onClose]); - if (!asValidURL(normalizedUrl)) { + const handleSubmit = React.useCallback(({ urls }: WebInputModalInputs) => { + // clean and prefix URLs + const cleanUrls = urls.reduce((acc, { value }) => { + const trimmed = (value || '').trim(); + if (!trimmed) return acc; // noinspection HttpUrlsUsage - addSnackbar({ - key: 'invalid-url', - message: 'Please enter a valid web address', - type: 'issue', - overrides: { autoHideDuration: 2000 }, - }); + const normalized = (!trimmed.startsWith('http://') && !trimmed.startsWith('https://')) + ? 'https://' + trimmed + : trimmed; + if (asValidURL(normalized)) + acc.push(normalized); + return acc; + }, [] as string[]); + if (!cleanUrls.length) { + addSnackbar({ key: 'invalid-urls', message: 'Please enter at least one valid web address', type: 'issue', overrides: { autoHideDuration: 2000 } }); return; } - - onURLSubmit(normalizedUrl); + onWebLinks(cleanUrls); handleClose(); - }, [handleClose, onURLSubmit]); + }, [handleClose, onWebLinks]); return ( @@ -66,51 +77,72 @@ function WebInputModal(props: { hideBottomClose > - Enter or paste a web page address to import its content. + Enter or paste web page addresses to import their content. Works on most websites and for YouTube videos (e.g., youtube.com/...) the transcript will be imported. + {/*You can add up to {MAX_URLS} URLs.*/}
- - ( - - : undefined} - value={value} - onChange={onChange} - /> - {error && {error.message}} - - )} /> - - - - {/**/} - {/* Cancel*/} - {/**/} + + {formFields.map((field, index) => ( + ( + + + : undefined} + value={value} + onChange={onChange} + sx={{ flex: 1 }} + /> + {urlFieldCount > 1 && ( + formFieldsRemove(index)} + > + + + )} + + {error && {error.message}} + + )} + /> + ))} + + + {/* Add a new link */} + + + {formIsDirty && } @@ -121,7 +153,7 @@ function WebInputModal(props: { } -export function useWebInputModal(onAttachWeb: (url: string) => void) { +export function useWebInputModal(onAttachWebLinks: (urls: string[]) => void) { // state const [open, setOpen] = React.useState(false); @@ -131,9 +163,9 @@ export function useWebInputModal(onAttachWeb: (url: string) => void) { const webInputDialogComponent = React.useMemo(() => open && ( setOpen(false)} - onURLSubmit={onAttachWeb} + onWebLinks={onAttachWebLinks} /> - ), [onAttachWeb, open]); + ), [onAttachWebLinks, open]); return { openWebInputDialog,