Skip to content

Commit

Permalink
Web Input: multiball
Browse files Browse the repository at this point in the history
  • Loading branch information
enricoros committed Jan 12, 2025
1 parent 2b9a5b5 commit 213241e
Show file tree
Hide file tree
Showing 2 changed files with 98 additions and 64 deletions.
6 changes: 4 additions & 2 deletions src/apps/chat/components/composer/Composer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
156 changes: 94 additions & 62 deletions src/apps/chat/components/composer/WebInputModal.tsx
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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<WebInputModalInputs>({
values: { url: '' },
mode: 'onChange', // validate on change
const { control: formControl, handleSubmit: formHandleSubmit, formState: { isValid: formIsValid, isDirty: formIsDirty } } = useForm<WebInputModalInputs>({
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 (
Expand All @@ -66,51 +77,72 @@ function WebInputModal(props: {
hideBottomClose
>
<Box fontSize='sm'>
Enter or paste a web page address to import its content.
Enter or paste web page addresses to import their content.
</Box>
<Typography level='body-sm'>
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.*/}
</Typography>

<form onSubmit={formHandleSubmit(handleSubmit)}>

<Controller
control={formControl}
name='url'
rules={{ required: 'Please enter a valid URL' }}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl error={!!error}>
<Input
autoFocus
required
placeholder='https://...'
endDecorator={extractYoutubeVideoIDFromURL(value) ? <YouTubeIcon sx={{ color: 'red' }} /> : undefined}
value={value}
onChange={onChange}
/>
{error && <FormHelperText>{error.message}</FormHelperText>}
</FormControl>
)} />

<Box sx={{ display: 'flex', justifyContent: 'flex-end', gap: 1, mt: 2.5 }}>

{/*<Button*/}
{/* aria-label='Close Dialog'*/}
{/* variant='soft'*/}
{/* color='neutral'*/}
{/* onClick={handleClose}*/}
{/* sx={{ minWidth: 100 }}*/}
{/*>*/}
{/* Cancel*/}
{/*</Button>*/}
<Stack spacing={1}>
{formFields.map((field, index) => (
<Controller
key={field.id}
control={formControl}
name={`urls.${index}.value`}
rules={{ required: 'Please enter a valid URL' }}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl error={!!error}>
<Box sx={{ display: 'flex', gap: 1 }}>
<Input
autoFocus={index === 0}
required={index === 0}
placeholder='https://...'
endDecorator={extractYoutubeVideoIDFromURL(value) ? <YouTubeIcon sx={{ color: 'red' }} /> : undefined}
value={value}
onChange={onChange}
sx={{ flex: 1 }}
/>
{urlFieldCount > 1 && (
<IconButton
size='sm'
variant='plain'
color='neutral'
onClick={() => formFieldsRemove(index)}
>
<DeleteOutlineIcon />
</IconButton>
)}
</Box>
{error && <FormHelperText>{error.message}</FormHelperText>}
</FormControl>
)}
/>
))}
</Stack>

{/* Add a new link */}
<Box sx={{ display: 'flex', justifyContent: 'space-between', gap: 1, mt: 2.5 }}>

{formIsDirty && <Button
color='neutral'
variant='soft'
disabled={urlFieldCount >= MAX_URLS}
onClick={() => formFieldsAppend({ value: '' })}
startDecorator={<AddIcon />}
>
Another
{/*{urlFieldCount >= MAX_URLS ? 'Enough URLs' : urlFieldCount === 1 ? 'Add URL' : urlFieldCount === 2 ? 'Add another' : urlFieldCount === 3 ? 'And another one' : urlFieldCount === 4 ? 'Why stopping' : 'Just one more'}*/}
</Button>}

<Button
variant='solid'
type='submit'
disabled={!formIsValid}
sx={{ minWidth: 160 }}
disabled={!formIsValid || !formIsDirty}
sx={{ minWidth: 160, ml: 'auto' }}
>
Add
Add {urlFieldCount > 1 ? `(${urlFieldCount})` : ''}
</Button>

</Box>
Expand All @@ -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);
Expand All @@ -131,9 +163,9 @@ export function useWebInputModal(onAttachWeb: (url: string) => void) {
const webInputDialogComponent = React.useMemo(() => open && (
<WebInputModal
onClose={() => setOpen(false)}
onURLSubmit={onAttachWeb}
onWebLinks={onAttachWebLinks}
/>
), [onAttachWeb, open]);
), [onAttachWebLinks, open]);

return {
openWebInputDialog,
Expand Down

0 comments on commit 213241e

Please sign in to comment.