mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-30 23:28:52 +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
|
|
@ -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';
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue