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,
|
returnFile: true,
|
||||||
});
|
});
|
||||||
filepath = result.filepath;
|
filepath = result.filepath;
|
||||||
|
width = result.width;
|
||||||
|
height = result.height;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileInfo = removeNullishValues({
|
const fileInfo = removeNullishValues({
|
||||||
|
|
|
||||||
|
|
@ -350,6 +350,7 @@ export type TAskProps = {
|
||||||
conversationId?: string | null;
|
conversationId?: string | null;
|
||||||
messageId?: string | null;
|
messageId?: string | null;
|
||||||
clientTimestamp?: string;
|
clientTimestamp?: string;
|
||||||
|
toolResources?: t.AgentToolResources;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TOptions = {
|
export type TOptions = {
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { AutoSizer, List } from 'react-virtualized';
|
||||||
import { Spinner, useCombobox } from '@librechat/client';
|
import { Spinner, useCombobox } from '@librechat/client';
|
||||||
import { useSetRecoilState, useRecoilValue } from 'recoil';
|
import { useSetRecoilState, useRecoilValue } from 'recoil';
|
||||||
import { PermissionTypes, Permissions } from 'librechat-data-provider';
|
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 type { PromptOption } from '~/common';
|
||||||
import { removeCharIfLast, detectVariables } from '~/utils';
|
import { removeCharIfLast, detectVariables } from '~/utils';
|
||||||
import VariableDialog from '~/components/Prompts/Groups/VariableDialog';
|
import VariableDialog from '~/components/Prompts/Groups/VariableDialog';
|
||||||
|
|
@ -51,7 +51,7 @@ function PromptsCommand({
|
||||||
}: {
|
}: {
|
||||||
index: number;
|
index: number;
|
||||||
textAreaRef: React.MutableRefObject<HTMLTextAreaElement | null>;
|
textAreaRef: React.MutableRefObject<HTMLTextAreaElement | null>;
|
||||||
submitPrompt: (textPrompt: string) => void;
|
submitPrompt: (textPrompt: string, toolResources?: AgentToolResources) => void;
|
||||||
}) {
|
}) {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
const hasAccess = useHasAccess({
|
const hasAccess = useHasAccess({
|
||||||
|
|
@ -79,7 +79,10 @@ function PromptsCommand({
|
||||||
|
|
||||||
const handleSelect = useCallback(
|
const handleSelect = useCallback(
|
||||||
(mention?: PromptOption, e?: React.KeyboardEvent<HTMLInputElement>) => {
|
(mention?: PromptOption, e?: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
console.log('PromptsCommand.handleSelect called with mention:', mention);
|
||||||
|
|
||||||
if (!mention) {
|
if (!mention) {
|
||||||
|
console.log('No mention provided');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -92,10 +95,19 @@ function PromptsCommand({
|
||||||
}
|
}
|
||||||
|
|
||||||
const group = promptsMap?.[mention.id];
|
const group = promptsMap?.[mention.id];
|
||||||
|
console.log('Found group for mention:', group);
|
||||||
if (!group) {
|
if (!group) {
|
||||||
|
console.log('No group found for mention ID:', mention.id);
|
||||||
return;
|
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 ?? '');
|
const hasVariables = detectVariables(group.productionPrompt?.prompt ?? '');
|
||||||
if (hasVariables) {
|
if (hasVariables) {
|
||||||
if (e && e.key === 'Tab') {
|
if (e && e.key === 'Tab') {
|
||||||
|
|
@ -105,7 +117,16 @@ function PromptsCommand({
|
||||||
setVariableDialogOpen(true);
|
setVariableDialogOpen(true);
|
||||||
return;
|
return;
|
||||||
} else {
|
} 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],
|
[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 { 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 {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
|
|
@ -37,18 +37,39 @@ function ChatGroupItem({
|
||||||
const { hasPermission } = useResourcePermissions('promptGroup', group._id || '');
|
const { hasPermission } = useResourcePermissions('promptGroup', group._id || '');
|
||||||
const canEdit = hasPermission(PermissionBits.EDIT);
|
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> = () => {
|
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;
|
const text = group.productionPrompt?.prompt;
|
||||||
if (!text?.trim()) {
|
if (!text?.trim()) {
|
||||||
|
console.log('No prompt text found');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (detectVariables(text)) {
|
if (detectVariables(text)) {
|
||||||
|
console.log('Prompt has variables, opening dialog');
|
||||||
setVariableDialogOpen(true);
|
setVariableDialogOpen(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
submitPrompt(text);
|
console.log('Calling submitPrompt with tool_resources');
|
||||||
|
submitPrompt(text, group.productionPrompt?.tool_resources);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -57,6 +78,7 @@ function ChatGroupItem({
|
||||||
name={group.name}
|
name={group.name}
|
||||||
category={group.category ?? ''}
|
category={group.category ?? ''}
|
||||||
onClick={onCardClick}
|
onClick={onCardClick}
|
||||||
|
hasFiles={hasFiles}
|
||||||
snippet={
|
snippet={
|
||||||
typeof group.oneliner === 'string' && group.oneliner.length > 0
|
typeof group.oneliner === 'string' && group.oneliner.length > 0
|
||||||
? group.oneliner
|
? group.oneliner
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import React, { useState, useCallback, useMemo, useEffect } from 'react';
|
||||||
import { useRecoilState } from 'recoil';
|
import { useRecoilState } from 'recoil';
|
||||||
import { ListFilter, User, Share2 } from 'lucide-react';
|
import { ListFilter, User, Share2 } from 'lucide-react';
|
||||||
import { SystemCategories } from 'librechat-data-provider';
|
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 type { Option } from '~/common';
|
||||||
import { useLocalize, useCategories } from '~/hooks';
|
import { useLocalize, useCategories } from '~/hooks';
|
||||||
import { usePromptGroupsContext } from '~/Providers';
|
import { usePromptGroupsContext } from '~/Providers';
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Label } from '@librechat/client';
|
import { Label } from '@librechat/client';
|
||||||
|
import { Paperclip } from 'lucide-react';
|
||||||
import CategoryIcon from '~/components/Prompts/Groups/CategoryIcon';
|
import CategoryIcon from '~/components/Prompts/Groups/CategoryIcon';
|
||||||
|
|
||||||
export default function ListCard({
|
export default function ListCard({
|
||||||
|
|
@ -8,12 +9,14 @@ export default function ListCard({
|
||||||
snippet,
|
snippet,
|
||||||
onClick,
|
onClick,
|
||||||
children,
|
children,
|
||||||
|
hasFiles,
|
||||||
}: {
|
}: {
|
||||||
category: string;
|
category: string;
|
||||||
name: string;
|
name: string;
|
||||||
snippet: string;
|
snippet: string;
|
||||||
onClick?: React.MouseEventHandler<HTMLDivElement | HTMLButtonElement>;
|
onClick?: React.MouseEventHandler<HTMLDivElement | HTMLButtonElement>;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
|
hasFiles?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement | HTMLButtonElement>) => {
|
const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement | HTMLButtonElement>) => {
|
||||||
if (event.key === 'Enter' || event.key === ' ') {
|
if (event.key === 'Enter' || event.key === ' ') {
|
||||||
|
|
@ -43,6 +46,14 @@ export default function ListCard({
|
||||||
>
|
>
|
||||||
{name}
|
{name}
|
||||||
</Label>
|
</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>
|
||||||
<div>{children}</div>
|
<div>{children}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -133,7 +133,7 @@ export default function VariableForm({
|
||||||
text = text.replace(regex, value);
|
text = text.replace(regex, value);
|
||||||
});
|
});
|
||||||
|
|
||||||
submitPrompt(text);
|
submitPrompt(text, group.productionPrompt?.tool_resources);
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import supersub from 'remark-supersub';
|
||||||
import { Label } from '@librechat/client';
|
import { Label } from '@librechat/client';
|
||||||
import rehypeHighlight from 'rehype-highlight';
|
import rehypeHighlight from 'rehype-highlight';
|
||||||
import { replaceSpecialVars } from 'librechat-data-provider';
|
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 { codeNoExecution } from '~/components/Chat/Messages/Content/MarkdownComponents';
|
||||||
import { useLocalize, useAuthContext } from '~/hooks';
|
import { useLocalize, useAuthContext } from '~/hooks';
|
||||||
import CategoryIcon from './Groups/CategoryIcon';
|
import CategoryIcon from './Groups/CategoryIcon';
|
||||||
|
|
@ -15,6 +15,7 @@ import PromptVariables from './PromptVariables';
|
||||||
import { PromptVariableGfm } from './Markdown';
|
import { PromptVariableGfm } from './Markdown';
|
||||||
import Description from './Description';
|
import Description from './Description';
|
||||||
import Command from './Command';
|
import Command from './Command';
|
||||||
|
import PromptFilesPreview from './PromptFilesPreview';
|
||||||
|
|
||||||
const PromptDetails = ({ group }: { group?: TPromptGroup }) => {
|
const PromptDetails = ({ group }: { group?: TPromptGroup }) => {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
|
|
@ -25,6 +26,17 @@ const PromptDetails = ({ group }: { group?: TPromptGroup }) => {
|
||||||
return replaceSpecialVars({ text: initialText, user });
|
return replaceSpecialVars({ text: initialText, user });
|
||||||
}, [group?.productionPrompt?.prompt, 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) {
|
if (!group) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -72,6 +84,7 @@ const PromptDetails = ({ group }: { group?: TPromptGroup }) => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<PromptVariables promptText={mainText} showInfo={false} />
|
<PromptVariables promptText={mainText} showInfo={false} />
|
||||||
|
{hasFiles && toolResources && <PromptFilesPreview toolResources={toolResources} />}
|
||||||
<Description initialValue={group.oneliner} disabled={true} />
|
<Description initialValue={group.oneliner} disabled={true} />
|
||||||
<Command initialValue={group.command} disabled={true} />
|
<Command initialValue={group.command} disabled={true} />
|
||||||
</div>
|
</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 EmptyPromptPreview } from './EmptyPromptPreview';
|
||||||
export { default as PromptSidePanel } from './Groups/GroupSidePanel';
|
export { default as PromptSidePanel } from './Groups/GroupSidePanel';
|
||||||
export { default as CreatePromptForm } from './Groups/CreatePromptForm';
|
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) {
|
if (group) {
|
||||||
queryClient.setQueryData<t.PromptGroupListData>(
|
queryClient.setQueryData<t.PromptGroupListData>(
|
||||||
[QueryKeys.promptGroups, name, category, pageSize],
|
[QueryKeys.promptGroups, name, category, pageSize],
|
||||||
|
|
@ -163,6 +165,8 @@ export const useAddPromptToGroup = (
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
queryClient.invalidateQueries([QueryKeys.files]);
|
||||||
|
|
||||||
if (onSuccess) {
|
if (onSuccess) {
|
||||||
onSuccess(response, variables, context);
|
onSuccess(response, variables, context);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { v4 } from 'uuid';
|
import { v4 } from 'uuid';
|
||||||
import { cloneDeep } from 'lodash';
|
import { cloneDeep } from 'lodash';
|
||||||
|
import { useMemo } from 'react';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import {
|
import {
|
||||||
Constants,
|
Constants,
|
||||||
|
|
@ -213,17 +214,18 @@ export default function useChatFunctions({
|
||||||
submissionFiles.length > 0;
|
submissionFiles.length > 0;
|
||||||
|
|
||||||
if (setFiles && reuseFiles === true) {
|
if (setFiles && reuseFiles === true) {
|
||||||
currentMsg.files = [...submissionFiles];
|
currentMsg.files = submissionFiles;
|
||||||
setFiles(new Map());
|
setFiles(new Map());
|
||||||
setFilesToDelete({});
|
setFilesToDelete({});
|
||||||
} else if (setFiles && files && files.size > 0) {
|
} 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,
|
file_id: file.file_id,
|
||||||
filepath: file.filepath,
|
filepath: file.filepath,
|
||||||
type: file.type ?? '', // Ensure type is not undefined
|
type: file.type ?? '', // Ensure type is not undefined
|
||||||
height: file.height,
|
height: file.height,
|
||||||
width: file.width,
|
width: file.width,
|
||||||
}));
|
}));
|
||||||
|
currentMsg.files = chatFiles;
|
||||||
setFiles(new Map());
|
setFiles(new Map());
|
||||||
setFilesToDelete({});
|
setFilesToDelete({});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,19 @@ export default function useUpdateFiles(setFiles: FileSetter) {
|
||||||
const setFilesToDelete = useSetFilesToDelete();
|
const setFilesToDelete = useSetFilesToDelete();
|
||||||
|
|
||||||
const addFile = (newFile: ExtendedFile) => {
|
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) => {
|
setFiles((currentFiles) => {
|
||||||
|
console.log('Current files before adding:', Array.from(currentFiles.keys()));
|
||||||
const updatedFiles = new Map(currentFiles);
|
const updatedFiles = new Map(currentFiles);
|
||||||
updatedFiles.set(newFile.file_id, newFile);
|
updatedFiles.set(newFile.file_id, newFile);
|
||||||
|
console.log('Files after adding:', Array.from(updatedFiles.keys()));
|
||||||
return updatedFiles;
|
return updatedFiles;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,13 @@
|
||||||
import { v4 } from 'uuid';
|
import { v4 } from 'uuid';
|
||||||
import { useCallback } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||||
import { Constants, replaceSpecialVars } from 'librechat-data-provider';
|
import { Constants, replaceSpecialVars } from 'librechat-data-provider';
|
||||||
|
import type { AgentToolResources, TFile } from 'librechat-data-provider';
|
||||||
import { useChatContext, useChatFormContext, useAddedChatContext } from '~/Providers';
|
import { useChatContext, useChatFormContext, useAddedChatContext } from '~/Providers';
|
||||||
import { useAuthContext } from '~/hooks/AuthContext';
|
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';
|
import store from '~/store';
|
||||||
|
|
||||||
const appendIndex = (index: number, value?: string) => {
|
const appendIndex = (index: number, value?: string) => {
|
||||||
|
|
@ -16,15 +20,87 @@ const appendIndex = (index: number, value?: string) => {
|
||||||
export default function useSubmitMessage() {
|
export default function useSubmitMessage() {
|
||||||
const { user } = useAuthContext();
|
const { user } = useAuthContext();
|
||||||
const methods = useChatFormContext();
|
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 { addedIndex, ask: askAdditional, conversation: addedConvo } = useAddedChatContext();
|
||||||
|
const { data: allFiles = [] } = useGetFiles();
|
||||||
|
const { addFile } = useUpdateFiles(setFiles);
|
||||||
|
|
||||||
const autoSendPrompts = useRecoilValue(store.autoSendPrompts);
|
const autoSendPrompts = useRecoilValue(store.autoSendPrompts);
|
||||||
const activeConvos = useRecoilValue(store.allConversationsSelector);
|
const activeConvos = useRecoilValue(store.allConversationsSelector);
|
||||||
const setActivePrompt = useSetRecoilState(store.activePromptByIndex(index));
|
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(
|
const submitMessage = useCallback(
|
||||||
(data?: { text: string }) => {
|
(data?: { text: string; toolResources?: AgentToolResources }) => {
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return console.warn('No data provided to submitMessage');
|
return console.warn('No data provided to submitMessage');
|
||||||
}
|
}
|
||||||
|
|
@ -51,6 +127,7 @@ export default function useSubmitMessage() {
|
||||||
overrideConvoId: appendIndex(rootIndex, overrideConvoId),
|
overrideConvoId: appendIndex(rootIndex, overrideConvoId),
|
||||||
overrideUserMessageId: appendIndex(rootIndex, overrideUserMessageId),
|
overrideUserMessageId: appendIndex(rootIndex, overrideUserMessageId),
|
||||||
clientTimestamp,
|
clientTimestamp,
|
||||||
|
toolResources: data.toolResources,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (hasAdded) {
|
if (hasAdded) {
|
||||||
|
|
@ -60,6 +137,7 @@ export default function useSubmitMessage() {
|
||||||
overrideConvoId: appendIndex(addedIndex, overrideConvoId),
|
overrideConvoId: appendIndex(addedIndex, overrideConvoId),
|
||||||
overrideUserMessageId: appendIndex(addedIndex, overrideUserMessageId),
|
overrideUserMessageId: appendIndex(addedIndex, overrideUserMessageId),
|
||||||
clientTimestamp,
|
clientTimestamp,
|
||||||
|
toolResources: data.toolResources,
|
||||||
},
|
},
|
||||||
{ overrideMessages: rootMessages },
|
{ overrideMessages: rootMessages },
|
||||||
);
|
);
|
||||||
|
|
@ -80,18 +158,60 @@ export default function useSubmitMessage() {
|
||||||
);
|
);
|
||||||
|
|
||||||
const submitPrompt = useCallback(
|
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 });
|
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) {
|
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 });
|
submitMessage({ text: parsedText });
|
||||||
return;
|
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 currentText = methods.getValues('text');
|
||||||
const newText = currentText.trim().length > 1 ? `\n${parsedText}` : parsedText;
|
const newText = currentText.trim().length > 1 ? `\n${parsedText}` : parsedText;
|
||||||
setActivePrompt(newText);
|
setActivePrompt(newText);
|
||||||
},
|
},
|
||||||
[autoSendPrompts, submitMessage, setActivePrompt, methods, user],
|
[
|
||||||
|
autoSendPrompts,
|
||||||
|
submitMessage,
|
||||||
|
setActivePrompt,
|
||||||
|
methods,
|
||||||
|
user,
|
||||||
|
addFile,
|
||||||
|
convertToolResourcesToFiles,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
return { submitMessage, submitPrompt };
|
return { submitMessage, submitPrompt };
|
||||||
|
|
|
||||||
|
|
@ -143,11 +143,13 @@ export const usePromptFileHandling = (params?: UsePromptFileHandling) => {
|
||||||
(extendedFile: ExtendedFile, preview: string) => {
|
(extendedFile: ExtendedFile, preview: string) => {
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
img.onload = async () => {
|
img.onload = async () => {
|
||||||
|
// Update the file with dimensions
|
||||||
|
extendedFile.width = img.width;
|
||||||
|
extendedFile.height = img.height;
|
||||||
|
extendedFile.progress = 0.6;
|
||||||
|
|
||||||
const updatedFile = {
|
const updatedFile = {
|
||||||
...extendedFile,
|
...extendedFile,
|
||||||
width: img.width,
|
|
||||||
height: img.height,
|
|
||||||
progress: 0.6,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
setFiles((prev) =>
|
setFiles((prev) =>
|
||||||
|
|
@ -357,7 +359,7 @@ export const usePromptFileHandling = (params?: UsePromptFileHandling) => {
|
||||||
filename: dbFile.filename,
|
filename: dbFile.filename,
|
||||||
filepath: dbFile.filepath,
|
filepath: dbFile.filepath,
|
||||||
progress: 1,
|
progress: 1,
|
||||||
preview: undefined, // Will be set by FilePreviewLoader
|
preview: dbFile.filepath, // Use filepath as preview for existing files
|
||||||
size: dbFile.bytes || 0,
|
size: dbFile.bytes || 0,
|
||||||
width: dbFile.width,
|
width: dbFile.width,
|
||||||
height: dbFile.height,
|
height: dbFile.height,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue