feat: chat ui and functionality for prompts (auto-send not working)

This commit is contained in:
Dustin Healy 2025-09-06 00:12:43 -07:00
parent 7c3356e10b
commit 607a5a2fcf
18 changed files with 636 additions and 19 deletions

View file

@ -702,6 +702,8 @@ const processAgentFileUpload = async ({ req, res, metadata }) => {
returnFile: true,
});
filepath = result.filepath;
width = result.width;
height = result.height;
}
const fileInfo = removeNullishValues({

View file

@ -350,6 +350,7 @@ export type TAskProps = {
conversationId?: string | null;
messageId?: string | null;
clientTimestamp?: string;
toolResources?: t.AgentToolResources;
};
export type TOptions = {

View file

@ -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],

View 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;

View 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;

View file

@ -0,0 +1,3 @@
export { default as PromptFileUpload } from './PromptFileUpload';
export { default as PromptFileRow } from './PromptFileRow';

View file

@ -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

View file

@ -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';

View file

@ -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>

View file

@ -133,7 +133,7 @@ export default function VariableForm({
text = text.replace(regex, value);
});
submitPrompt(text);
submitPrompt(text, group.productionPrompt?.tool_resources);
onClose();
};

View file

@ -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>

View 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;

View file

@ -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';

View file

@ -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);
}

View file

@ -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({});
}

View file

@ -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;
});
};

View file

@ -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 };

View file

@ -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,