mirror of
https://github.com/enricoros/big-AGI.git
synced 2026-05-10 21:50:14 -07:00
Web Input: multiball
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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<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' });
|
||||
|
||||
// derived
|
||||
const urlFieldCount = formFields.length;
|
||||
|
||||
|
||||
// handlers
|
||||
|
||||
const { onClose, onURLSubmit } = props;
|
||||
const { onClose, onWebLinks } = props;
|
||||
|
||||
const handleClose = React.useCallback(() => onClose(), [onClose]);
|
||||
|
||||
const handleSubmit = React.useCallback(({ url }: WebInputModalInputs) => {
|
||||
|
||||
let normalizedUrl = (url || '').trim();
|
||||
// noinspection HttpUrlsUsage
|
||||
if (!normalizedUrl.startsWith('http://') && !normalizedUrl.startsWith('https://'))
|
||||
normalizedUrl = 'https://' + normalizedUrl;
|
||||
|
||||
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
|
||||
>
|
||||
<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)}>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
)} />
|
||||
{/* Add a new link */}
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', gap: 1, mt: 2.5 }}>
|
||||
|
||||
<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>*/}
|
||||
{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>
|
||||
@@ -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 && (
|
||||
<WebInputModal
|
||||
onClose={() => setOpen(false)}
|
||||
onURLSubmit={onAttachWeb}
|
||||
onWebLinks={onAttachWebLinks}
|
||||
/>
|
||||
), [onAttachWeb, open]);
|
||||
), [onAttachWebLinks, open]);
|
||||
|
||||
return {
|
||||
openWebInputDialog,
|
||||
|
||||
Reference in New Issue
Block a user