mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-30 07:08:50 +01:00
feat: add file attachment section PromptFiles, new file display: PromptFile (needed for deletion to work properly), and usePromptFileHandling hook
This commit is contained in:
parent
600641d02f
commit
623dfa5b63
7 changed files with 758 additions and 4 deletions
|
|
@ -3,11 +3,12 @@ import { useNavigate } from 'react-router-dom';
|
|||
import { Button, TextareaAutosize, Input } from '@librechat/client';
|
||||
import { useForm, Controller, FormProvider } from 'react-hook-form';
|
||||
import { LocalStorageKeys, PermissionTypes, Permissions } from 'librechat-data-provider';
|
||||
import type { AgentToolResources } from 'librechat-data-provider';
|
||||
import PromptVariablesAndFiles from '~/components/Prompts/PromptVariablesAndFiles';
|
||||
import CategorySelector from '~/components/Prompts/Groups/CategorySelector';
|
||||
import { useLocalize, useHasAccess, usePromptFileHandling } from '~/hooks';
|
||||
import VariablesDropdown from '~/components/Prompts/VariablesDropdown';
|
||||
import PromptVariables from '~/components/Prompts/PromptVariables';
|
||||
import Description from '~/components/Prompts/Description';
|
||||
import { useLocalize, useHasAccess } from '~/hooks';
|
||||
import Command from '~/components/Prompts/Command';
|
||||
import { useCreatePrompt } from '~/data-provider';
|
||||
import { cn } from '~/utils';
|
||||
|
|
@ -19,6 +20,7 @@ type CreateFormValues = {
|
|||
category: string;
|
||||
oneliner?: string;
|
||||
command?: string;
|
||||
tool_resources?: AgentToolResources;
|
||||
};
|
||||
|
||||
const defaultPrompt: CreateFormValues = {
|
||||
|
|
@ -37,6 +39,15 @@ const CreatePromptForm = ({
|
|||
}) => {
|
||||
const localize = useLocalize();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const {
|
||||
promptFiles: files,
|
||||
setFiles,
|
||||
handleFileChange,
|
||||
handleFileRemove,
|
||||
getToolResources,
|
||||
} = usePromptFileHandling();
|
||||
|
||||
const hasAccess = useHasAccess({
|
||||
permissionType: PermissionTypes.PROMPTS,
|
||||
permission: Permissions.CREATE,
|
||||
|
|
@ -88,8 +99,15 @@ const CreatePromptForm = ({
|
|||
if ((command?.length ?? 0) > 0) {
|
||||
groupData.command = command;
|
||||
}
|
||||
|
||||
const promptData = { ...rest };
|
||||
const toolResources = getToolResources();
|
||||
if (toolResources) {
|
||||
promptData.tool_resources = toolResources;
|
||||
}
|
||||
|
||||
createPromptMutation.mutate({
|
||||
prompt: rest,
|
||||
prompt: promptData,
|
||||
group: groupData,
|
||||
});
|
||||
};
|
||||
|
|
@ -161,7 +179,14 @@ const CreatePromptForm = ({
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
<PromptVariables promptText={promptText} />
|
||||
<PromptVariablesAndFiles
|
||||
promptText={promptText}
|
||||
files={files}
|
||||
onFilesChange={setFiles}
|
||||
handleFileChange={handleFileChange}
|
||||
onFileRemove={handleFileRemove}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
<Description
|
||||
onValueChange={(value) => methods.setValue('oneliner', value)}
|
||||
tabIndex={0}
|
||||
|
|
|
|||
172
client/src/components/Prompts/PromptFile.tsx
Normal file
172
client/src/components/Prompts/PromptFile.tsx
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
import { useEffect } from 'react';
|
||||
import { useToastContext } from '@librechat/client';
|
||||
import { FileSources } from 'librechat-data-provider';
|
||||
import type { ExtendedFile } from '~/common';
|
||||
import FileContainer from '~/components/Chat/Input/Files/FileContainer';
|
||||
import { useDeleteFilesMutation } from '~/data-provider';
|
||||
import Image from '~/components/Chat/Input/Files/Image';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import { logger } from '~/utils';
|
||||
|
||||
export default function PromptFile({
|
||||
files: _files,
|
||||
setFiles,
|
||||
abortUpload,
|
||||
setFilesLoading,
|
||||
onFileRemove,
|
||||
fileFilter,
|
||||
isRTL = false,
|
||||
Wrapper,
|
||||
}: {
|
||||
files: Map<string, ExtendedFile> | undefined;
|
||||
abortUpload?: () => void;
|
||||
setFiles: React.Dispatch<React.SetStateAction<Map<string, ExtendedFile>>>;
|
||||
setFilesLoading: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
onFileRemove?: (fileId: string) => void;
|
||||
fileFilter?: (file: ExtendedFile) => boolean;
|
||||
isRTL?: boolean;
|
||||
Wrapper?: React.FC<{ children: React.ReactNode }>;
|
||||
}) {
|
||||
const localize = useLocalize();
|
||||
const { showToast } = useToastContext();
|
||||
const files = Array.from(_files?.values() ?? []).filter((file) =>
|
||||
fileFilter ? fileFilter(file) : true,
|
||||
);
|
||||
|
||||
const { mutateAsync } = useDeleteFilesMutation({
|
||||
onMutate: async () =>
|
||||
logger.log(
|
||||
'prompts',
|
||||
'Deleting prompt files',
|
||||
files.map((f) => f.file_id),
|
||||
),
|
||||
onSuccess: () => {
|
||||
console.log('Prompt files deleted');
|
||||
},
|
||||
onError: (error) => {
|
||||
console.log('Error deleting prompt files:', error);
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (files.length === 0) {
|
||||
setFilesLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (files.some((file) => file.progress < 1)) {
|
||||
setFilesLoading(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (files.every((file) => file.progress === 1)) {
|
||||
setFilesLoading(false);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [files]);
|
||||
|
||||
if (files.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const renderFiles = () => {
|
||||
const rowStyle = isRTL
|
||||
? {
|
||||
display: 'flex',
|
||||
flexDirection: 'row-reverse',
|
||||
flexWrap: 'wrap',
|
||||
gap: '4px',
|
||||
width: '100%',
|
||||
maxWidth: '100%',
|
||||
}
|
||||
: {
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: '4px',
|
||||
width: '100%',
|
||||
maxWidth: '100%',
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={rowStyle as React.CSSProperties}>
|
||||
{files
|
||||
.reduce(
|
||||
(acc, current) => {
|
||||
if (!acc.map.has(current.file_id)) {
|
||||
acc.map.set(current.file_id, true);
|
||||
acc.uniqueFiles.push(current);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{ map: new Map(), uniqueFiles: [] as ExtendedFile[] },
|
||||
)
|
||||
.uniqueFiles.map((file: ExtendedFile, index: number) => {
|
||||
const handleDelete = () => {
|
||||
showToast({
|
||||
message: localize('com_ui_deleting_file'),
|
||||
status: 'info',
|
||||
});
|
||||
|
||||
if (abortUpload && file.progress < 1) {
|
||||
abortUpload();
|
||||
}
|
||||
|
||||
if (onFileRemove) {
|
||||
onFileRemove(file.file_id);
|
||||
} else {
|
||||
mutateAsync({
|
||||
files: [
|
||||
{
|
||||
file_id: file.file_id,
|
||||
filepath: file.filepath || '',
|
||||
embedded: file.embedded || false,
|
||||
source: file.source || FileSources.local,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
setFiles((currentFiles) => {
|
||||
const updatedFiles = new Map(currentFiles);
|
||||
updatedFiles.delete(file.file_id);
|
||||
if (file.temp_file_id) {
|
||||
updatedFiles.delete(file.temp_file_id);
|
||||
}
|
||||
return updatedFiles;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const isImage = file.type?.startsWith('image') ?? false;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
style={{
|
||||
flexBasis: '70px',
|
||||
flexGrow: 0,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
{isImage ? (
|
||||
<Image
|
||||
url={file.preview ?? file.filepath}
|
||||
onDelete={handleDelete}
|
||||
progress={file.progress}
|
||||
source={file.source}
|
||||
/>
|
||||
) : (
|
||||
<FileContainer file={file} onDelete={handleDelete} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
if (Wrapper) {
|
||||
return <Wrapper>{renderFiles()}</Wrapper>;
|
||||
}
|
||||
|
||||
return renderFiles();
|
||||
}
|
||||
81
client/src/components/Prompts/PromptFiles.tsx
Normal file
81
client/src/components/Prompts/PromptFiles.tsx
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
import { useMemo } from 'react';
|
||||
import { FileText } from 'lucide-react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import { Separator } from '@librechat/client';
|
||||
import type { ExtendedFile } from '~/common';
|
||||
import AttachFileButton from '~/components/Prompts/Files/AttachFileButton';
|
||||
import PromptFile from '~/components/Prompts/PromptFile';
|
||||
import { useLocalize } from '~/hooks';
|
||||
|
||||
const PromptFiles = ({
|
||||
files,
|
||||
onFilesChange,
|
||||
handleFileChange,
|
||||
onFileRemove,
|
||||
disabled,
|
||||
}: {
|
||||
files: ExtendedFile[];
|
||||
onFilesChange?: (files: ExtendedFile[]) => void;
|
||||
handleFileChange?: (event: React.ChangeEvent<HTMLInputElement>, toolResource?: string) => void;
|
||||
onFileRemove?: (fileId: string) => void;
|
||||
disabled?: boolean;
|
||||
}) => {
|
||||
const localize = useLocalize();
|
||||
|
||||
const filesMap = useMemo(() => {
|
||||
const map = new Map<string, ExtendedFile>();
|
||||
files.forEach((file) => {
|
||||
const key = file.file_id || file.temp_file_id || '';
|
||||
if (key) {
|
||||
map.set(key, file);
|
||||
}
|
||||
});
|
||||
return map;
|
||||
}, [files]);
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col rounded-xl border border-border-light bg-transparent p-4 shadow-md">
|
||||
<h3 className="flex items-center gap-2 py-2 text-lg font-semibold text-text-primary">
|
||||
<FileText className="icon-sm" aria-hidden="true" />
|
||||
{localize('com_ui_files')}
|
||||
</h3>
|
||||
<div className="flex flex-1 flex-col space-y-4">
|
||||
{!files.length && (
|
||||
<div className="text-sm text-text-secondary">
|
||||
<ReactMarkdown className="markdown prose dark:prose-invert">
|
||||
{localize('com_ui_files_info')}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{files.length > 0 && (
|
||||
<div className="mb-3 flex-1">
|
||||
<PromptFile
|
||||
files={filesMap}
|
||||
setFiles={(newMapOrUpdater) => {
|
||||
const newMap =
|
||||
typeof newMapOrUpdater === 'function'
|
||||
? newMapOrUpdater(filesMap)
|
||||
: newMapOrUpdater;
|
||||
const newFiles = Array.from(newMap.values()) as ExtendedFile[];
|
||||
onFilesChange?.(newFiles);
|
||||
}}
|
||||
setFilesLoading={() => {}}
|
||||
onFileRemove={onFileRemove}
|
||||
Wrapper={({ children }) => <div className="flex flex-wrap gap-2">{children}</div>}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Separator className="my-3 text-text-primary" />
|
||||
<div className="flex flex-col justify-end text-text-secondary">
|
||||
<div className="flex justify-start">
|
||||
<AttachFileButton handleFileChange={handleFileChange} disabled={disabled} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PromptFiles;
|
||||
46
client/src/components/Prompts/PromptVariablesAndFiles.tsx
Normal file
46
client/src/components/Prompts/PromptVariablesAndFiles.tsx
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import React from 'react';
|
||||
import type { ExtendedFile } from '~/common';
|
||||
import PromptVariables from './PromptVariables';
|
||||
import PromptFiles from './PromptFiles';
|
||||
|
||||
interface PromptVariablesAndFilesProps {
|
||||
promptText: string;
|
||||
files?: ExtendedFile[];
|
||||
onFilesChange?: (files: ExtendedFile[]) => void;
|
||||
handleFileChange?: (event: React.ChangeEvent<HTMLInputElement>, toolResource?: string) => void;
|
||||
onFileRemove?: (fileId: string) => void;
|
||||
disabled?: boolean;
|
||||
showVariablesInfo?: boolean;
|
||||
}
|
||||
|
||||
const PromptVariablesAndFiles: React.FC<PromptVariablesAndFilesProps> = ({
|
||||
promptText,
|
||||
files = [],
|
||||
onFilesChange,
|
||||
handleFileChange,
|
||||
onFileRemove,
|
||||
disabled,
|
||||
showVariablesInfo = true,
|
||||
}) => {
|
||||
return (
|
||||
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2 lg:items-stretch">
|
||||
{/* Variables Section */}
|
||||
<div className="w-full">
|
||||
<PromptVariables promptText={promptText} showInfo={showVariablesInfo} />
|
||||
</div>
|
||||
|
||||
{/* Files Section */}
|
||||
<div className="w-full">
|
||||
<PromptFiles
|
||||
files={files}
|
||||
onFilesChange={onFilesChange}
|
||||
handleFileChange={handleFileChange}
|
||||
onFileRemove={onFileRemove}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PromptVariablesAndFiles;
|
||||
Loading…
Add table
Add a link
Reference in a new issue