mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-19 09:50:15 +01:00
feat: chat ui and functionality for prompts (auto-send not working)
This commit is contained in:
parent
7c3356e10b
commit
607a5a2fcf
18 changed files with 636 additions and 19 deletions
|
|
@ -702,6 +702,8 @@ const processAgentFileUpload = async ({ req, res, metadata }) => {
|
|||
returnFile: true,
|
||||
});
|
||||
filepath = result.filepath;
|
||||
width = result.width;
|
||||
height = result.height;
|
||||
}
|
||||
|
||||
const fileInfo = removeNullishValues({
|
||||
|
|
|
|||
|
|
@ -350,6 +350,7 @@ export type TAskProps = {
|
|||
conversationId?: string | null;
|
||||
messageId?: string | null;
|
||||
clientTimestamp?: string;
|
||||
toolResources?: t.AgentToolResources;
|
||||
};
|
||||
|
||||
export type TOptions = {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { AutoSizer, List } from 'react-virtualized';
|
|||
import { Spinner, useCombobox } from '@librechat/client';
|
||||
import { useSetRecoilState, useRecoilValue } from 'recoil';
|
||||
import { PermissionTypes, Permissions } from 'librechat-data-provider';
|
||||
import type { TPromptGroup } from 'librechat-data-provider';
|
||||
import type { TPromptGroup, AgentToolResources } from 'librechat-data-provider';
|
||||
import type { PromptOption } from '~/common';
|
||||
import { removeCharIfLast, detectVariables } from '~/utils';
|
||||
import VariableDialog from '~/components/Prompts/Groups/VariableDialog';
|
||||
|
|
@ -51,7 +51,7 @@ function PromptsCommand({
|
|||
}: {
|
||||
index: number;
|
||||
textAreaRef: React.MutableRefObject<HTMLTextAreaElement | null>;
|
||||
submitPrompt: (textPrompt: string) => void;
|
||||
submitPrompt: (textPrompt: string, toolResources?: AgentToolResources) => void;
|
||||
}) {
|
||||
const localize = useLocalize();
|
||||
const hasAccess = useHasAccess({
|
||||
|
|
@ -79,7 +79,10 @@ function PromptsCommand({
|
|||
|
||||
const handleSelect = useCallback(
|
||||
(mention?: PromptOption, e?: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
console.log('PromptsCommand.handleSelect called with mention:', mention);
|
||||
|
||||
if (!mention) {
|
||||
console.log('No mention provided');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -92,10 +95,19 @@ function PromptsCommand({
|
|||
}
|
||||
|
||||
const group = promptsMap?.[mention.id];
|
||||
console.log('Found group for mention:', group);
|
||||
if (!group) {
|
||||
console.log('No group found for mention ID:', mention.id);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Group productionPrompt details:', {
|
||||
hasProductionPrompt: !!group.productionPrompt,
|
||||
prompt: group.productionPrompt?.prompt?.substring(0, 100) + '...',
|
||||
tool_resources: group.productionPrompt?.tool_resources,
|
||||
hasToolResources: !!group.productionPrompt?.tool_resources,
|
||||
});
|
||||
|
||||
const hasVariables = detectVariables(group.productionPrompt?.prompt ?? '');
|
||||
if (hasVariables) {
|
||||
if (e && e.key === 'Tab') {
|
||||
|
|
@ -105,7 +117,16 @@ function PromptsCommand({
|
|||
setVariableDialogOpen(true);
|
||||
return;
|
||||
} else {
|
||||
submitPrompt(group.productionPrompt?.prompt ?? '');
|
||||
console.log('PromptsCommand - Clicking prompt:', {
|
||||
promptName: group.name,
|
||||
promptText: group.productionPrompt?.prompt,
|
||||
toolResources: group.productionPrompt?.tool_resources,
|
||||
hasToolResources: !!group.productionPrompt?.tool_resources,
|
||||
toolResourcesKeys: group.productionPrompt?.tool_resources
|
||||
? Object.keys(group.productionPrompt.tool_resources)
|
||||
: [],
|
||||
});
|
||||
submitPrompt(group.productionPrompt?.prompt ?? '', group.productionPrompt?.tool_resources);
|
||||
}
|
||||
},
|
||||
[setSearchValue, setOpen, setShowPromptsPopover, textAreaRef, promptsMap, submitPrompt],
|
||||
|
|
|
|||
86
client/src/components/Prompts/Files/PromptFileRow.tsx
Normal file
86
client/src/components/Prompts/Files/PromptFileRow.tsx
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
import React from 'react';
|
||||
import { X, FileText, Image, Upload } from 'lucide-react';
|
||||
import type { ExtendedFile } from 'librechat-data-provider';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import { cn } from '~/utils';
|
||||
|
||||
interface PromptFileRowProps {
|
||||
files: ExtendedFile[];
|
||||
onRemoveFile: (fileId: string) => void;
|
||||
isReadOnly?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const PromptFileRow: React.FC<PromptFileRowProps> = ({
|
||||
files,
|
||||
onRemoveFile,
|
||||
isReadOnly = false,
|
||||
className = '',
|
||||
}) => {
|
||||
const localize = useLocalize();
|
||||
|
||||
if (files.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const getFileIcon = (file: ExtendedFile) => {
|
||||
if (file.type?.startsWith('image/')) {
|
||||
return <Image className="h-4 w-4" />;
|
||||
}
|
||||
return <FileText className="h-4 w-4" />;
|
||||
};
|
||||
|
||||
const getFileStatus = (file: ExtendedFile) => {
|
||||
if (file.progress < 1) {
|
||||
return (
|
||||
<div className="flex items-center gap-1 text-xs text-blue-600">
|
||||
<Upload className="h-3 w-3 animate-pulse" />
|
||||
{Math.round(file.progress * 100)}%
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn('flex flex-wrap gap-2', className)}>
|
||||
{files.map((file) => (
|
||||
<div
|
||||
key={file.temp_file_id || file.file_id}
|
||||
className={cn(
|
||||
'flex items-center gap-2 rounded-lg border px-3 py-2 text-sm',
|
||||
'border-border-medium bg-surface-secondary',
|
||||
file.progress < 1 && 'opacity-70',
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{getFileIcon(file)}
|
||||
<span className="max-w-32 truncate" title={file.filename}>
|
||||
{file.filename}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{getFileStatus(file)}
|
||||
|
||||
{!isReadOnly && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onRemoveFile(file.temp_file_id || file.file_id || '')}
|
||||
className={cn(
|
||||
'ml-1 flex h-5 w-5 items-center justify-center rounded-full',
|
||||
'hover:bg-surface-hover text-text-secondary hover:text-text-primary',
|
||||
'transition-colors duration-200',
|
||||
)}
|
||||
title={localize('com_ui_remove_file')}
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PromptFileRow;
|
||||
|
||||
198
client/src/components/Prompts/Files/PromptFileUpload.tsx
Normal file
198
client/src/components/Prompts/Files/PromptFileUpload.tsx
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
import React, { useRef, useState, useMemo } from 'react';
|
||||
import { Paperclip, Upload, Folder } from 'lucide-react';
|
||||
import {
|
||||
Button,
|
||||
TooltipAnchor,
|
||||
AttachmentIcon,
|
||||
DropdownPopup,
|
||||
FileUpload,
|
||||
} from '@librechat/client';
|
||||
import { EToolResources } from 'librechat-data-provider';
|
||||
import type { ExtendedFile } from 'librechat-data-provider';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import { usePromptFileHandling } from '~/hooks/Prompts';
|
||||
import PromptFileRow from './PromptFileRow';
|
||||
import { cn } from '~/utils';
|
||||
import * as Ariakit from '@ariakit/react';
|
||||
|
||||
interface PromptFileUploadProps {
|
||||
files: ExtendedFile[];
|
||||
onFilesChange: (files: ExtendedFile[]) => void;
|
||||
onToolResourcesChange?: (toolResources: any) => void;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
variant?: 'button' | 'icon';
|
||||
showFileList?: boolean;
|
||||
}
|
||||
|
||||
const PromptFileUpload: React.FC<PromptFileUploadProps> = ({
|
||||
files,
|
||||
onFilesChange,
|
||||
onToolResourcesChange,
|
||||
disabled = false,
|
||||
className = '',
|
||||
variant = 'button',
|
||||
showFileList = true,
|
||||
}) => {
|
||||
const localize = useLocalize();
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const [isPopoverActive, setIsPopoverActive] = useState(false);
|
||||
const [toolResource, setToolResource] = useState<string>(EToolResources.file_search);
|
||||
|
||||
const { handleFileChange, promptFiles, getToolResources, areFilesReady, fileStats } =
|
||||
usePromptFileHandling({
|
||||
fileSetter: onFilesChange,
|
||||
initialFiles: files,
|
||||
});
|
||||
|
||||
// Update parent component when files change
|
||||
React.useEffect(() => {
|
||||
if (onToolResourcesChange && areFilesReady) {
|
||||
const toolResources = getToolResources();
|
||||
onToolResourcesChange(toolResources);
|
||||
}
|
||||
}, [promptFiles, areFilesReady, getToolResources, onToolResourcesChange]);
|
||||
|
||||
const handleUploadClick = (isImage?: boolean) => {
|
||||
if (isImage) {
|
||||
setToolResource(EToolResources.image_edit);
|
||||
} else {
|
||||
setToolResource(EToolResources.file_search);
|
||||
}
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.click();
|
||||
}
|
||||
};
|
||||
|
||||
const handleButtonClick = () => {
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.click();
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveFile = (fileId: string) => {
|
||||
const updatedFiles = promptFiles.filter(
|
||||
(file) => file.temp_file_id !== fileId && file.file_id !== fileId,
|
||||
);
|
||||
onFilesChange(updatedFiles);
|
||||
};
|
||||
|
||||
const dropdownItems = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
label: localize('com_ui_upload_file_search'),
|
||||
onClick: () => handleUploadClick(false),
|
||||
icon: <Folder className="icon-md" />,
|
||||
},
|
||||
{
|
||||
label: localize('com_ui_upload_ocr_text'),
|
||||
onClick: () => handleUploadClick(true),
|
||||
icon: <AttachmentIcon className="icon-md" />,
|
||||
},
|
||||
];
|
||||
}, [localize]);
|
||||
|
||||
const getButtonText = () => {
|
||||
if (fileStats.uploading > 0) {
|
||||
return `${localize('com_ui_uploading')} (${fileStats.uploading})`;
|
||||
}
|
||||
if (fileStats.total > 0) {
|
||||
return `${fileStats.total} ${localize('com_ui_files_attached')}`;
|
||||
}
|
||||
return localize('com_ui_attach_files');
|
||||
};
|
||||
|
||||
const menuTrigger = (
|
||||
<TooltipAnchor
|
||||
render={
|
||||
<Ariakit.MenuButton
|
||||
disabled={disabled}
|
||||
id="prompt-attach-file-menu-button"
|
||||
aria-label="Attach File Options"
|
||||
className={cn(
|
||||
'flex size-9 items-center justify-center rounded-full p-1 transition-colors hover:bg-surface-hover focus:outline-none focus:ring-2 focus:ring-primary focus:ring-opacity-50',
|
||||
)}
|
||||
>
|
||||
<div className="flex w-full items-center justify-center gap-2">
|
||||
<AttachmentIcon />
|
||||
</div>
|
||||
</Ariakit.MenuButton>
|
||||
}
|
||||
id="prompt-attach-file-menu-button"
|
||||
description={localize('com_sidepanel_attach_files')}
|
||||
disabled={disabled}
|
||||
/>
|
||||
);
|
||||
|
||||
if (variant === 'icon') {
|
||||
return (
|
||||
<>
|
||||
<FileUpload
|
||||
ref={fileInputRef}
|
||||
handleFileChange={(e) => {
|
||||
handleFileChange(e, toolResource);
|
||||
}}
|
||||
>
|
||||
<DropdownPopup
|
||||
menuId="prompt-attach-file-menu"
|
||||
className="overflow-visible"
|
||||
isOpen={isPopoverActive}
|
||||
setIsOpen={setIsPopoverActive}
|
||||
modal={true}
|
||||
unmountOnHide={true}
|
||||
trigger={menuTrigger}
|
||||
items={dropdownItems}
|
||||
iconClassName="mr-0"
|
||||
/>
|
||||
</FileUpload>
|
||||
|
||||
{showFileList && (
|
||||
<PromptFileRow files={promptFiles} onRemoveFile={handleRemoveFile} className="mt-2" />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<FileUpload
|
||||
ref={fileInputRef}
|
||||
handleFileChange={(e) => {
|
||||
handleFileChange(e, toolResource);
|
||||
}}
|
||||
>
|
||||
<DropdownPopup
|
||||
menuId="prompt-attach-file-menu-button"
|
||||
className="overflow-visible"
|
||||
isOpen={isPopoverActive}
|
||||
setIsOpen={setIsPopoverActive}
|
||||
modal={true}
|
||||
unmountOnHide={true}
|
||||
trigger={
|
||||
<Button
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
variant="outline"
|
||||
className={cn('flex items-center gap-2', fileStats.uploading > 0 && 'opacity-70')}
|
||||
>
|
||||
{fileStats.uploading > 0 ? (
|
||||
<Upload className="h-4 w-4 animate-pulse" />
|
||||
) : (
|
||||
<AttachmentIcon className="h-4 w-4" />
|
||||
)}
|
||||
{getButtonText()}
|
||||
</Button>
|
||||
}
|
||||
items={dropdownItems}
|
||||
iconClassName="mr-0"
|
||||
/>
|
||||
</FileUpload>
|
||||
|
||||
{showFileList && promptFiles.length > 0 && (
|
||||
<PromptFileRow files={promptFiles} onRemoveFile={handleRemoveFile} className="mt-3" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PromptFileUpload;
|
||||
3
client/src/components/Prompts/Files/index.ts
Normal file
3
client/src/components/Prompts/Files/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export { default as PromptFileUpload } from './PromptFileUpload';
|
||||
export { default as PromptFileRow } from './PromptFileRow';
|
||||
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { useState, useMemo, memo } from 'react';
|
||||
import { Menu as MenuIcon, Edit as EditIcon, EarthIcon, TextSearch } from 'lucide-react';
|
||||
import { Menu as MenuIcon, Edit as EditIcon, EarthIcon, TextSearch, Paperclip } from 'lucide-react';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuItem,
|
||||
|
|
@ -37,18 +37,39 @@ function ChatGroupItem({
|
|||
const { hasPermission } = useResourcePermissions('promptGroup', group._id || '');
|
||||
const canEdit = hasPermission(PermissionBits.EDIT);
|
||||
|
||||
// Check if prompt has attached files
|
||||
const hasFiles = useMemo(() => {
|
||||
const toolResources = group.productionPrompt?.tool_resources;
|
||||
if (!toolResources) return false;
|
||||
|
||||
return Object.values(toolResources).some(
|
||||
(resource) => resource?.file_ids && resource.file_ids.length > 0,
|
||||
);
|
||||
}, [group.productionPrompt?.tool_resources]);
|
||||
|
||||
const onCardClick: React.MouseEventHandler<HTMLButtonElement> = () => {
|
||||
console.log('ChatGroupItem.onCardClick called for:', group.name);
|
||||
console.log('Group productionPrompt:', {
|
||||
hasPrompt: !!group.productionPrompt?.prompt,
|
||||
prompt: group.productionPrompt?.prompt?.substring(0, 100) + '...',
|
||||
tool_resources: group.productionPrompt?.tool_resources,
|
||||
hasToolResources: !!group.productionPrompt?.tool_resources,
|
||||
});
|
||||
|
||||
const text = group.productionPrompt?.prompt;
|
||||
if (!text?.trim()) {
|
||||
console.log('No prompt text found');
|
||||
return;
|
||||
}
|
||||
|
||||
if (detectVariables(text)) {
|
||||
console.log('Prompt has variables, opening dialog');
|
||||
setVariableDialogOpen(true);
|
||||
return;
|
||||
}
|
||||
|
||||
submitPrompt(text);
|
||||
console.log('Calling submitPrompt with tool_resources');
|
||||
submitPrompt(text, group.productionPrompt?.tool_resources);
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -57,6 +78,7 @@ function ChatGroupItem({
|
|||
name={group.name}
|
||||
category={group.category ?? ''}
|
||||
onClick={onCardClick}
|
||||
hasFiles={hasFiles}
|
||||
snippet={
|
||||
typeof group.oneliner === 'string' && group.oneliner.length > 0
|
||||
? group.oneliner
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import React, { useState, useCallback, useMemo, useEffect } from 'react';
|
|||
import { useRecoilState } from 'recoil';
|
||||
import { ListFilter, User, Share2 } from 'lucide-react';
|
||||
import { SystemCategories } from 'librechat-data-provider';
|
||||
import { Dropdown, AnimatedSearchInput } from '@librechat/client';
|
||||
import { Dropdown, AnimatedSearchInput, AttachmentIcon } from '@librechat/client';
|
||||
import type { Option } from '~/common';
|
||||
import { useLocalize, useCategories } from '~/hooks';
|
||||
import { usePromptGroupsContext } from '~/Providers';
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import React from 'react';
|
||||
import { Label } from '@librechat/client';
|
||||
import { Paperclip } from 'lucide-react';
|
||||
import CategoryIcon from '~/components/Prompts/Groups/CategoryIcon';
|
||||
|
||||
export default function ListCard({
|
||||
|
|
@ -8,12 +9,14 @@ export default function ListCard({
|
|||
snippet,
|
||||
onClick,
|
||||
children,
|
||||
hasFiles,
|
||||
}: {
|
||||
category: string;
|
||||
name: string;
|
||||
snippet: string;
|
||||
onClick?: React.MouseEventHandler<HTMLDivElement | HTMLButtonElement>;
|
||||
children?: React.ReactNode;
|
||||
hasFiles?: boolean;
|
||||
}) {
|
||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement | HTMLButtonElement>) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
|
|
@ -43,6 +46,14 @@ export default function ListCard({
|
|||
>
|
||||
{name}
|
||||
</Label>
|
||||
{/* Sometimes the paperclip renders a bit smaller in some entries compared to others, need to find cause before i mark ready for review */}
|
||||
{hasFiles && (
|
||||
<Paperclip
|
||||
className="h-4 w-4 text-text-secondary"
|
||||
aria-label="Has attached files"
|
||||
title="This prompt has attached files"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -133,7 +133,7 @@ export default function VariableForm({
|
|||
text = text.replace(regex, value);
|
||||
});
|
||||
|
||||
submitPrompt(text);
|
||||
submitPrompt(text, group.productionPrompt?.tool_resources);
|
||||
onClose();
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import supersub from 'remark-supersub';
|
|||
import { Label } from '@librechat/client';
|
||||
import rehypeHighlight from 'rehype-highlight';
|
||||
import { replaceSpecialVars } from 'librechat-data-provider';
|
||||
import type { TPromptGroup } from 'librechat-data-provider';
|
||||
import type { TPromptGroup, AgentToolResources } from 'librechat-data-provider';
|
||||
import { codeNoExecution } from '~/components/Chat/Messages/Content/MarkdownComponents';
|
||||
import { useLocalize, useAuthContext } from '~/hooks';
|
||||
import CategoryIcon from './Groups/CategoryIcon';
|
||||
|
|
@ -15,6 +15,7 @@ import PromptVariables from './PromptVariables';
|
|||
import { PromptVariableGfm } from './Markdown';
|
||||
import Description from './Description';
|
||||
import Command from './Command';
|
||||
import PromptFilesPreview from './PromptFilesPreview';
|
||||
|
||||
const PromptDetails = ({ group }: { group?: TPromptGroup }) => {
|
||||
const localize = useLocalize();
|
||||
|
|
@ -25,6 +26,17 @@ const PromptDetails = ({ group }: { group?: TPromptGroup }) => {
|
|||
return replaceSpecialVars({ text: initialText, user });
|
||||
}, [group?.productionPrompt?.prompt, user]);
|
||||
|
||||
const toolResources = useMemo(() => {
|
||||
return group?.productionPrompt?.tool_resources;
|
||||
}, [group?.productionPrompt?.tool_resources]);
|
||||
|
||||
const hasFiles = useMemo(() => {
|
||||
if (!toolResources) return false;
|
||||
return Object.values(toolResources).some(
|
||||
(resource) => resource?.file_ids && resource.file_ids.length > 0,
|
||||
);
|
||||
}, [toolResources]);
|
||||
|
||||
if (!group) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -72,6 +84,7 @@ const PromptDetails = ({ group }: { group?: TPromptGroup }) => {
|
|||
</div>
|
||||
</div>
|
||||
<PromptVariables promptText={mainText} showInfo={false} />
|
||||
{hasFiles && toolResources && <PromptFilesPreview toolResources={toolResources} />}
|
||||
<Description initialValue={group.oneliner} disabled={true} />
|
||||
<Command initialValue={group.command} disabled={true} />
|
||||
</div>
|
||||
|
|
|
|||
120
client/src/components/Prompts/PromptFilesPreview.tsx
Normal file
120
client/src/components/Prompts/PromptFilesPreview.tsx
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
import React, { useMemo } from 'react';
|
||||
import { Paperclip, FileText, Image, FileType } from 'lucide-react';
|
||||
import type { AgentToolResources } from 'librechat-data-provider';
|
||||
import { useGetFiles } from '~/data-provider';
|
||||
import { useLocalize } from '~/hooks';
|
||||
|
||||
interface PromptFilesPreviewProps {
|
||||
toolResources: AgentToolResources;
|
||||
}
|
||||
|
||||
const PromptFilesPreview: React.FC<PromptFilesPreviewProps> = ({ toolResources }) => {
|
||||
const localize = useLocalize();
|
||||
const { data: allFiles = [] } = useGetFiles();
|
||||
|
||||
// Create a fileMap for quick lookup
|
||||
const fileMap = useMemo(() => {
|
||||
const map: Record<string, any> = {};
|
||||
allFiles.forEach((file) => {
|
||||
if (file.file_id) {
|
||||
map[file.file_id] = file;
|
||||
}
|
||||
});
|
||||
return map;
|
||||
}, [allFiles]);
|
||||
|
||||
// Extract all file IDs from tool resources
|
||||
const attachedFiles = useMemo(() => {
|
||||
const files: Array<{ file: any; toolResource: string }> = [];
|
||||
|
||||
Object.entries(toolResources).forEach(([toolResource, resource]) => {
|
||||
if (resource?.file_ids) {
|
||||
resource.file_ids.forEach((fileId) => {
|
||||
const dbFile = fileMap[fileId];
|
||||
if (dbFile) {
|
||||
files.push({ file: dbFile, toolResource });
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return files;
|
||||
}, [toolResources, fileMap]);
|
||||
|
||||
const getFileIcon = (type: string) => {
|
||||
if (type?.startsWith('image/')) {
|
||||
return <Image className="h-4 w-4" />;
|
||||
}
|
||||
if (type?.includes('text') || type?.includes('document')) {
|
||||
return <FileText className="h-4 w-4" />;
|
||||
}
|
||||
return <FileType className="h-4 w-4" />;
|
||||
};
|
||||
|
||||
const getToolResourceLabel = (toolResource: string) => {
|
||||
switch (toolResource) {
|
||||
case 'file_search':
|
||||
return 'File Search';
|
||||
case 'execute_code':
|
||||
return 'Code Interpreter';
|
||||
case 'ocr':
|
||||
return 'Text Extraction';
|
||||
case 'image_edit':
|
||||
return 'Image Editing';
|
||||
default:
|
||||
return toolResource;
|
||||
}
|
||||
};
|
||||
|
||||
if (attachedFiles.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className="flex items-center justify-between rounded-t-lg border border-border-light py-2 pl-4 text-base font-semibold text-text-primary">
|
||||
<div className="flex items-center gap-2">
|
||||
<Paperclip className="h-4 w-4" />
|
||||
{localize('com_ui_files')} ({attachedFiles.length})
|
||||
</div>
|
||||
</h2>
|
||||
<div className="rounded-b-lg border border-border-light p-4">
|
||||
<div className="space-y-3">
|
||||
{attachedFiles.map(({ file, toolResource }, index) => (
|
||||
<div
|
||||
key={`${file.file_id}-${index}`}
|
||||
className="flex items-center justify-between rounded-lg border border-border-light p-3 transition-colors hover:bg-surface-tertiary"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-surface-secondary text-text-secondary">
|
||||
{getFileIcon(file.type)}
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-sm font-medium text-text-primary" title={file.filename}>
|
||||
{file.filename}
|
||||
</span>
|
||||
<div className="flex items-center gap-2 text-xs text-text-secondary">
|
||||
<span>{getToolResourceLabel(toolResource)}</span>
|
||||
{file.bytes && (
|
||||
<>
|
||||
<span>•</span>
|
||||
<span>{(file.bytes / 1024).toFixed(1)} KB</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{file.type?.startsWith('image/') && file.width && file.height && (
|
||||
<div className="text-xs text-text-secondary">
|
||||
{file.width} × {file.height}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PromptFilesPreview;
|
||||
|
|
@ -8,3 +8,5 @@ export { default as DashGroupItem } from './Groups/DashGroupItem';
|
|||
export { default as EmptyPromptPreview } from './EmptyPromptPreview';
|
||||
export { default as PromptSidePanel } from './Groups/GroupSidePanel';
|
||||
export { default as CreatePromptForm } from './Groups/CreatePromptForm';
|
||||
export { default as PromptVariablesAndFiles } from './PromptVariablesAndFiles';
|
||||
export { default as PromptFiles } from './PromptFiles';
|
||||
|
|
|
|||
|
|
@ -118,6 +118,8 @@ export const useCreatePrompt = (
|
|||
},
|
||||
);
|
||||
|
||||
queryClient.invalidateQueries([QueryKeys.files]);
|
||||
|
||||
if (group) {
|
||||
queryClient.setQueryData<t.PromptGroupListData>(
|
||||
[QueryKeys.promptGroups, name, category, pageSize],
|
||||
|
|
@ -163,6 +165,8 @@ export const useAddPromptToGroup = (
|
|||
},
|
||||
);
|
||||
|
||||
queryClient.invalidateQueries([QueryKeys.files]);
|
||||
|
||||
if (onSuccess) {
|
||||
onSuccess(response, variables, context);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { v4 } from 'uuid';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { useMemo } from 'react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
Constants,
|
||||
|
|
@ -213,17 +214,18 @@ export default function useChatFunctions({
|
|||
submissionFiles.length > 0;
|
||||
|
||||
if (setFiles && reuseFiles === true) {
|
||||
currentMsg.files = [...submissionFiles];
|
||||
currentMsg.files = submissionFiles;
|
||||
setFiles(new Map());
|
||||
setFilesToDelete({});
|
||||
} else if (setFiles && files && files.size > 0) {
|
||||
currentMsg.files = Array.from(files.values()).map((file) => ({
|
||||
const chatFiles = Array.from(files.values()).map((file) => ({
|
||||
file_id: file.file_id,
|
||||
filepath: file.filepath,
|
||||
type: file.type ?? '', // Ensure type is not undefined
|
||||
height: file.height,
|
||||
width: file.width,
|
||||
}));
|
||||
currentMsg.files = chatFiles;
|
||||
setFiles(new Map());
|
||||
setFilesToDelete({});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,9 +5,19 @@ export default function useUpdateFiles(setFiles: FileSetter) {
|
|||
const setFilesToDelete = useSetFilesToDelete();
|
||||
|
||||
const addFile = (newFile: ExtendedFile) => {
|
||||
console.log('useUpdateFiles.addFile called with:', {
|
||||
file_id: newFile.file_id,
|
||||
filename: newFile.filename,
|
||||
type: newFile.type,
|
||||
size: newFile.size,
|
||||
progress: newFile.progress,
|
||||
attached: newFile.attached,
|
||||
});
|
||||
setFiles((currentFiles) => {
|
||||
console.log('Current files before adding:', Array.from(currentFiles.keys()));
|
||||
const updatedFiles = new Map(currentFiles);
|
||||
updatedFiles.set(newFile.file_id, newFile);
|
||||
console.log('Files after adding:', Array.from(updatedFiles.keys()));
|
||||
return updatedFiles;
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,9 +1,13 @@
|
|||
import { v4 } from 'uuid';
|
||||
import { useCallback } from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
import { Constants, replaceSpecialVars } from 'librechat-data-provider';
|
||||
import type { AgentToolResources, TFile } from 'librechat-data-provider';
|
||||
import { useChatContext, useChatFormContext, useAddedChatContext } from '~/Providers';
|
||||
import { useAuthContext } from '~/hooks/AuthContext';
|
||||
import { useGetFiles } from '~/data-provider';
|
||||
import useUpdateFiles from '~/hooks/Files/useUpdateFiles';
|
||||
import type { ExtendedFile } from '~/common';
|
||||
import store from '~/store';
|
||||
|
||||
const appendIndex = (index: number, value?: string) => {
|
||||
|
|
@ -16,15 +20,87 @@ const appendIndex = (index: number, value?: string) => {
|
|||
export default function useSubmitMessage() {
|
||||
const { user } = useAuthContext();
|
||||
const methods = useChatFormContext();
|
||||
const { ask, index, getMessages, setMessages, latestMessage } = useChatContext();
|
||||
const { ask, index, getMessages, setMessages, latestMessage, setFiles } = useChatContext();
|
||||
const { addedIndex, ask: askAdditional, conversation: addedConvo } = useAddedChatContext();
|
||||
const { data: allFiles = [] } = useGetFiles();
|
||||
const { addFile } = useUpdateFiles(setFiles);
|
||||
|
||||
const autoSendPrompts = useRecoilValue(store.autoSendPrompts);
|
||||
const activeConvos = useRecoilValue(store.allConversationsSelector);
|
||||
const setActivePrompt = useSetRecoilState(store.activePromptByIndex(index));
|
||||
|
||||
// Create a fileMap for quick lookup
|
||||
const fileMap = useMemo(() => {
|
||||
const map: Record<string, TFile> = {};
|
||||
allFiles.forEach((file) => {
|
||||
if (file.file_id) {
|
||||
map[file.file_id] = file;
|
||||
}
|
||||
});
|
||||
return map;
|
||||
}, [allFiles]);
|
||||
|
||||
// Convert toolResources to ExtendedFile objects for chat UI
|
||||
const convertToolResourcesToFiles = useCallback(
|
||||
(toolResources: AgentToolResources): ExtendedFile[] => {
|
||||
console.log('convertToolResourcesToFiles called with:', toolResources);
|
||||
console.log('Available fileMap keys:', Object.keys(fileMap));
|
||||
|
||||
const promptFiles: ExtendedFile[] = [];
|
||||
|
||||
Object.entries(toolResources).forEach(([toolResource, resource]) => {
|
||||
console.log(`Processing toolResource "${toolResource}":`, resource);
|
||||
if (resource?.file_ids) {
|
||||
console.log(`Found ${resource.file_ids.length} file_ids:`, resource.file_ids);
|
||||
resource.file_ids.forEach((fileId) => {
|
||||
const dbFile = fileMap[fileId];
|
||||
console.log(`Looking up fileId "${fileId}":`, dbFile ? 'FOUND' : 'NOT FOUND');
|
||||
if (dbFile) {
|
||||
console.log('Database file details:', {
|
||||
file_id: dbFile.file_id,
|
||||
filename: dbFile.filename,
|
||||
type: dbFile.type,
|
||||
bytes: dbFile.bytes,
|
||||
width: dbFile.width,
|
||||
height: dbFile.height,
|
||||
hasWidthHeight: !!(dbFile.width && dbFile.height),
|
||||
});
|
||||
const extendedFile = {
|
||||
file_id: dbFile.file_id,
|
||||
temp_file_id: dbFile.file_id,
|
||||
filename: dbFile.filename,
|
||||
filepath: dbFile.filepath,
|
||||
type: dbFile.type,
|
||||
size: dbFile.bytes,
|
||||
width: dbFile.width,
|
||||
height: dbFile.height,
|
||||
progress: 1, // Already uploaded
|
||||
attached: true,
|
||||
tool_resource: toolResource,
|
||||
preview: dbFile.type?.startsWith('image/') ? dbFile.filepath : undefined,
|
||||
};
|
||||
console.log('✅ Created ExtendedFile:', extendedFile);
|
||||
promptFiles.push(extendedFile);
|
||||
} else {
|
||||
console.warn(`File not found in fileMap: ${fileId}`);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.log(`⚠️ No file_ids in resource "${toolResource}"`);
|
||||
}
|
||||
});
|
||||
|
||||
console.log(
|
||||
`convertToolResourcesToFiles returning ${promptFiles.length} files:`,
|
||||
promptFiles,
|
||||
);
|
||||
return promptFiles;
|
||||
},
|
||||
[fileMap],
|
||||
);
|
||||
|
||||
const submitMessage = useCallback(
|
||||
(data?: { text: string }) => {
|
||||
(data?: { text: string; toolResources?: AgentToolResources }) => {
|
||||
if (!data) {
|
||||
return console.warn('No data provided to submitMessage');
|
||||
}
|
||||
|
|
@ -51,6 +127,7 @@ export default function useSubmitMessage() {
|
|||
overrideConvoId: appendIndex(rootIndex, overrideConvoId),
|
||||
overrideUserMessageId: appendIndex(rootIndex, overrideUserMessageId),
|
||||
clientTimestamp,
|
||||
toolResources: data.toolResources,
|
||||
});
|
||||
|
||||
if (hasAdded) {
|
||||
|
|
@ -60,6 +137,7 @@ export default function useSubmitMessage() {
|
|||
overrideConvoId: appendIndex(addedIndex, overrideConvoId),
|
||||
overrideUserMessageId: appendIndex(addedIndex, overrideUserMessageId),
|
||||
clientTimestamp,
|
||||
toolResources: data.toolResources,
|
||||
},
|
||||
{ overrideMessages: rootMessages },
|
||||
);
|
||||
|
|
@ -80,18 +158,60 @@ export default function useSubmitMessage() {
|
|||
);
|
||||
|
||||
const submitPrompt = useCallback(
|
||||
(text: string) => {
|
||||
(text: string, toolResources?: AgentToolResources) => {
|
||||
console.log('useSubmitMessage.submitPrompt called:', {
|
||||
text: text?.substring(0, 100) + '...',
|
||||
toolResources,
|
||||
hasToolResources: !!toolResources,
|
||||
autoSendPrompts,
|
||||
});
|
||||
|
||||
const parsedText = replaceSpecialVars({ text, user });
|
||||
|
||||
// ALWAYS add files to chat state first (like AttachFileMenu does)
|
||||
if (toolResources) {
|
||||
console.log('Converting toolResources to files...');
|
||||
const promptFiles = convertToolResourcesToFiles(toolResources);
|
||||
console.log('Converted files:', promptFiles);
|
||||
|
||||
// Add files to chat state so they appear in UI (same as AttachFileMenu)
|
||||
promptFiles.forEach((file, index) => {
|
||||
console.log(`Adding file ${index + 1}/${promptFiles.length}:`, {
|
||||
file_id: file.file_id,
|
||||
filename: file.filename,
|
||||
type: file.type,
|
||||
size: file.size,
|
||||
});
|
||||
addFile(file);
|
||||
});
|
||||
console.log('All files added to chat state');
|
||||
} else {
|
||||
console.log('No toolResources provided');
|
||||
}
|
||||
|
||||
if (autoSendPrompts) {
|
||||
console.log('Auto-sending message (files should be in chat state)');
|
||||
// Auto-send: files are now in chat state, submit without toolResources
|
||||
// (files will be picked up from chat state like AttachFileMenu)
|
||||
submitMessage({ text: parsedText });
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Manual mode: setting text in input (files should be visible in UI)');
|
||||
// Manual send: files are in chat state, just set text
|
||||
const currentText = methods.getValues('text');
|
||||
const newText = currentText.trim().length > 1 ? `\n${parsedText}` : parsedText;
|
||||
setActivePrompt(newText);
|
||||
},
|
||||
[autoSendPrompts, submitMessage, setActivePrompt, methods, user],
|
||||
[
|
||||
autoSendPrompts,
|
||||
submitMessage,
|
||||
setActivePrompt,
|
||||
methods,
|
||||
user,
|
||||
addFile,
|
||||
convertToolResourcesToFiles,
|
||||
],
|
||||
);
|
||||
|
||||
return { submitMessage, submitPrompt };
|
||||
|
|
|
|||
|
|
@ -143,11 +143,13 @@ export const usePromptFileHandling = (params?: UsePromptFileHandling) => {
|
|||
(extendedFile: ExtendedFile, preview: string) => {
|
||||
const img = new Image();
|
||||
img.onload = async () => {
|
||||
// Update the file with dimensions
|
||||
extendedFile.width = img.width;
|
||||
extendedFile.height = img.height;
|
||||
extendedFile.progress = 0.6;
|
||||
|
||||
const updatedFile = {
|
||||
...extendedFile,
|
||||
width: img.width,
|
||||
height: img.height,
|
||||
progress: 0.6,
|
||||
};
|
||||
|
||||
setFiles((prev) =>
|
||||
|
|
@ -357,7 +359,7 @@ export const usePromptFileHandling = (params?: UsePromptFileHandling) => {
|
|||
filename: dbFile.filename,
|
||||
filepath: dbFile.filepath,
|
||||
progress: 1,
|
||||
preview: undefined, // Will be set by FilePreviewLoader
|
||||
preview: dbFile.filepath, // Use filepath as preview for existing files
|
||||
size: dbFile.bytes || 0,
|
||||
width: dbFile.width,
|
||||
height: dbFile.height,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue