mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 17:00:15 +01:00
🗃️ feat: Code Interpreter File Persistence between Sessions (#6790)
* refactor: Enhance FileContainer with customizable button and container styles, onClick button handling, and type override * refactor: Update file type handling to support partial file objects * refactor: Extract download handling into a custom hook for improved reusability * refactor: Replace LogContent with Stdout component and enhance Attachment rendering for added visibility * feat: Update @librechat/agents to version 2.4.1 for referencing generated files in subsequent code interpreter uses * feat: Add support for tab-separated values (TSV) in mime type handling and improve error logging for regex patterns * chore: Update @librechat/agents to version 2.4.11 for better `session_id` instructions when wanting to persist files between executions * chore: Update @librechat/agents to version 2.4.12 for improved functionality * fix: Enhance argument parsing in useParseArgs to support JSON input and improve code extraction * refactor: Update input handling in useAutoSave to require more than one character before saving to local storage
This commit is contained in:
parent
910c73359b
commit
5d668748f9
12 changed files with 113 additions and 30 deletions
|
|
@ -49,7 +49,7 @@
|
||||||
"@langchain/google-genai": "^0.2.2",
|
"@langchain/google-genai": "^0.2.2",
|
||||||
"@langchain/google-vertexai": "^0.2.3",
|
"@langchain/google-vertexai": "^0.2.3",
|
||||||
"@langchain/textsplitters": "^0.1.0",
|
"@langchain/textsplitters": "^0.1.0",
|
||||||
"@librechat/agents": "^2.4.0",
|
"@librechat/agents": "^2.4.12",
|
||||||
"@librechat/data-schemas": "*",
|
"@librechat/data-schemas": "*",
|
||||||
"@waylaidwanderer/fetch-event-source": "^3.0.1",
|
"@waylaidwanderer/fetch-event-source": "^3.0.1",
|
||||||
"axios": "^1.8.2",
|
"axios": "^1.8.2",
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,40 @@
|
||||||
import type { TFile } from 'librechat-data-provider';
|
import type { TFile } from 'librechat-data-provider';
|
||||||
import type { ExtendedFile } from '~/common';
|
import type { ExtendedFile } from '~/common';
|
||||||
|
import { getFileType, cn } from '~/utils';
|
||||||
import FilePreview from './FilePreview';
|
import FilePreview from './FilePreview';
|
||||||
import RemoveFile from './RemoveFile';
|
import RemoveFile from './RemoveFile';
|
||||||
import { getFileType } from '~/utils';
|
|
||||||
|
|
||||||
const FileContainer = ({
|
const FileContainer = ({
|
||||||
file,
|
file,
|
||||||
|
overrideType,
|
||||||
|
buttonClassName,
|
||||||
|
containerClassName,
|
||||||
onDelete,
|
onDelete,
|
||||||
|
onClick,
|
||||||
}: {
|
}: {
|
||||||
file: ExtendedFile | TFile;
|
file: Partial<ExtendedFile | TFile>;
|
||||||
|
overrideType?: string;
|
||||||
|
buttonClassName?: string;
|
||||||
|
containerClassName?: string;
|
||||||
onDelete?: () => void;
|
onDelete?: () => void;
|
||||||
|
onClick?: React.MouseEventHandler<HTMLButtonElement>;
|
||||||
}) => {
|
}) => {
|
||||||
const fileType = getFileType(file.type);
|
const fileType = getFileType(overrideType ?? file.type);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="group relative inline-block text-sm text-text-primary">
|
<div
|
||||||
<div className="relative overflow-hidden rounded-2xl border border-border-light">
|
className={cn('group relative inline-block text-sm text-text-primary', containerClassName)}
|
||||||
<div className="w-56 bg-surface-hover-alt p-1.5">
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClick}
|
||||||
|
aria-label={file.filename}
|
||||||
|
className={cn(
|
||||||
|
'relative overflow-hidden rounded-2xl border border-border-light bg-surface-hover-alt',
|
||||||
|
buttonClassName,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="w-56 p-1.5">
|
||||||
<div className="flex flex-row items-center gap-2">
|
<div className="flex flex-row items-center gap-2">
|
||||||
<FilePreview file={file} fileType={fileType} className="relative" />
|
<FilePreview file={file} fileType={fileType} className="relative" />
|
||||||
<div className="overflow-hidden">
|
<div className="overflow-hidden">
|
||||||
|
|
@ -29,7 +47,7 @@ const FileContainer = ({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</button>
|
||||||
{onDelete && <RemoveFile onRemove={onDelete} />}
|
{onDelete && <RemoveFile onRemove={onDelete} />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ const FilePreview = ({
|
||||||
fileType,
|
fileType,
|
||||||
className = '',
|
className = '',
|
||||||
}: {
|
}: {
|
||||||
file?: ExtendedFile | TFile;
|
file?: Partial<ExtendedFile | TFile>;
|
||||||
fileType: {
|
fileType: {
|
||||||
paths: React.FC;
|
paths: React.FC;
|
||||||
fill: string;
|
fill: string;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,27 @@
|
||||||
|
import { memo } from 'react';
|
||||||
import { imageExtRegex } from 'librechat-data-provider';
|
import { imageExtRegex } from 'librechat-data-provider';
|
||||||
import type { TAttachment, TFile, TAttachmentMetadata } from 'librechat-data-provider';
|
import type { TAttachment, TFile, TAttachmentMetadata } from 'librechat-data-provider';
|
||||||
|
import FileContainer from '~/components/Chat/Input/Files/FileContainer';
|
||||||
import Image from '~/components/Chat/Messages/Content/Image';
|
import Image from '~/components/Chat/Messages/Content/Image';
|
||||||
|
import { useAttachmentLink } from './LogLink';
|
||||||
|
|
||||||
|
const FileAttachment = memo(({ attachment }: { attachment: TAttachment }) => {
|
||||||
|
const { handleDownload } = useAttachmentLink({
|
||||||
|
href: attachment.filepath,
|
||||||
|
filename: attachment.filename,
|
||||||
|
});
|
||||||
|
const extension = attachment.filename.split('.').pop();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FileContainer
|
||||||
|
file={attachment}
|
||||||
|
onClick={handleDownload}
|
||||||
|
overrideType={extension}
|
||||||
|
containerClassName="max-w-fit"
|
||||||
|
buttonClassName="hover:cursor-pointer hover:bg-surface-secondary active:bg-surface-secondary focus:bg-surface-secondary hover:border-border-heavy active:border-border-heavy"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
export default function Attachment({ attachment }: { attachment?: TAttachment }) {
|
export default function Attachment({ attachment }: { attachment?: TAttachment }) {
|
||||||
if (!attachment) {
|
if (!attachment) {
|
||||||
|
|
@ -21,5 +42,5 @@ export default function Attachment({ attachment }: { attachment?: TAttachment })
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return <FileAttachment attachment={attachment} />;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import MarkdownLite from '~/components/Chat/Messages/Content/MarkdownLite';
|
||||||
import { useProgress, useLocalize } from '~/hooks';
|
import { useProgress, useLocalize } from '~/hooks';
|
||||||
import { CodeInProgress } from './CodeProgress';
|
import { CodeInProgress } from './CodeProgress';
|
||||||
import Attachment from './Attachment';
|
import Attachment from './Attachment';
|
||||||
import LogContent from './LogContent';
|
import Stdout from './Stdout';
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
|
||||||
interface ParsedArgs {
|
interface ParsedArgs {
|
||||||
|
|
@ -17,8 +17,17 @@ interface ParsedArgs {
|
||||||
|
|
||||||
export function useParseArgs(args: string): ParsedArgs {
|
export function useParseArgs(args: string): ParsedArgs {
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
|
let parsedArgs: ParsedArgs | string = args;
|
||||||
|
try {
|
||||||
|
parsedArgs = JSON.parse(args);
|
||||||
|
} catch {
|
||||||
|
// console.error('Failed to parse args:', e);
|
||||||
|
}
|
||||||
|
if (typeof parsedArgs === 'object') {
|
||||||
|
return parsedArgs;
|
||||||
|
}
|
||||||
const langMatch = args.match(/"lang"\s*:\s*"(\w+)"/);
|
const langMatch = args.match(/"lang"\s*:\s*"(\w+)"/);
|
||||||
const codeMatch = args.match(/"code"\s*:\s*"(.+?)(?="\s*,\s*"args"|$)/s);
|
const codeMatch = args.match(/"code"\s*:\s*"(.+?)(?="\s*,\s*"(session_id|args)"|"\s*})/s);
|
||||||
|
|
||||||
let code = '';
|
let code = '';
|
||||||
if (codeMatch) {
|
if (codeMatch) {
|
||||||
|
|
@ -26,7 +35,7 @@ export function useParseArgs(args: string): ParsedArgs {
|
||||||
if (code.endsWith('"}')) {
|
if (code.endsWith('"}')) {
|
||||||
code = code.slice(0, -2);
|
code = code.slice(0, -2);
|
||||||
}
|
}
|
||||||
code = code.replace(/\\n/g, '\n').replace(/\\/g, '');
|
code = code.replace(/\\n/g, '\n').replace(/\\"/g, '"').replace(/\\\\/g, '\\');
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -99,15 +108,17 @@ export default function ExecuteCode({
|
||||||
color: 'white',
|
color: 'white',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<pre className="shrink-0">
|
<Stdout output={output} />
|
||||||
<LogContent output={output} attachments={attachments} />
|
|
||||||
</pre>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{attachments?.map((attachment, index) => <Attachment attachment={attachment} key={index} />)}
|
<div className="mb-2 flex flex-wrap items-center gap-2.5">
|
||||||
|
{attachments?.map((attachment, index) => (
|
||||||
|
<Attachment attachment={attachment} key={index} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,11 @@ interface LogLinkProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const LogLink: React.FC<LogLinkProps> = ({ href, filename, children }) => {
|
export const useAttachmentLink = ({ href, filename }: Pick<LogLinkProps, 'href' | 'filename'>) => {
|
||||||
const { showToast } = useToastContext();
|
const { showToast } = useToastContext();
|
||||||
const { refetch: downloadFile } = useCodeOutputDownload(href);
|
const { refetch: downloadFile } = useCodeOutputDownload(href);
|
||||||
|
|
||||||
const handleDownload = async (event: React.MouseEvent<HTMLAnchorElement>) => {
|
const handleDownload = async (event: React.MouseEvent<HTMLAnchorElement | HTMLButtonElement>) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
try {
|
try {
|
||||||
const stream = await downloadFile();
|
const stream = await downloadFile();
|
||||||
|
|
@ -36,6 +36,11 @@ const LogLink: React.FC<LogLinkProps> = ({ href, filename, children }) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
return { handleDownload };
|
||||||
|
};
|
||||||
|
|
||||||
|
const LogLink: React.FC<LogLinkProps> = ({ href, filename, children }) => {
|
||||||
|
const { handleDownload } = useAttachmentLink({ href, filename });
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
href={href}
|
href={href}
|
||||||
|
|
|
||||||
26
client/src/components/Chat/Messages/Content/Parts/Stdout.tsx
Normal file
26
client/src/components/Chat/Messages/Content/Parts/Stdout.tsx
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
import React, { useMemo } from 'react';
|
||||||
|
|
||||||
|
interface StdoutProps {
|
||||||
|
output?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Stdout: React.FC<StdoutProps> = ({ output = '' }) => {
|
||||||
|
const processedContent = useMemo(() => {
|
||||||
|
if (!output) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts = output.split('Generated files:');
|
||||||
|
return parts[0].trim();
|
||||||
|
}, [output]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
processedContent && (
|
||||||
|
<pre className="shrink-0">
|
||||||
|
<div>{processedContent}</div>
|
||||||
|
</pre>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Stdout;
|
||||||
|
|
@ -5,7 +5,7 @@ export default function FileIcon({
|
||||||
file,
|
file,
|
||||||
fileType,
|
fileType,
|
||||||
}: {
|
}: {
|
||||||
file?: ExtendedFile | TFile;
|
file?: Partial<ExtendedFile | TFile>;
|
||||||
fileType: {
|
fileType: {
|
||||||
fill: string;
|
fill: string;
|
||||||
paths: React.FC;
|
paths: React.FC;
|
||||||
|
|
|
||||||
|
|
@ -128,7 +128,7 @@ export const useAutoSave = ({
|
||||||
|
|
||||||
const handleInput = debounce((e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
const handleInput = debounce((e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
const value = e.target.value;
|
const value = e.target.value;
|
||||||
if (value) {
|
if (value && value.length > 1) {
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
`${LocalStorageKeys.TEXT_DRAFT}${conversationId}`,
|
`${LocalStorageKeys.TEXT_DRAFT}${conversationId}`,
|
||||||
encodeBase64(value),
|
encodeBase64(value),
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,7 @@ export const fileTypes = {
|
||||||
title: 'File',
|
title: 'File',
|
||||||
},
|
},
|
||||||
text: textDocument,
|
text: textDocument,
|
||||||
|
txt: textDocument,
|
||||||
// application:,
|
// application:,
|
||||||
|
|
||||||
/* Partial matches */
|
/* Partial matches */
|
||||||
|
|
|
||||||
14
package-lock.json
generated
14
package-lock.json
generated
|
|
@ -65,7 +65,7 @@
|
||||||
"@langchain/google-genai": "^0.2.2",
|
"@langchain/google-genai": "^0.2.2",
|
||||||
"@langchain/google-vertexai": "^0.2.3",
|
"@langchain/google-vertexai": "^0.2.3",
|
||||||
"@langchain/textsplitters": "^0.1.0",
|
"@langchain/textsplitters": "^0.1.0",
|
||||||
"@librechat/agents": "^2.4.0",
|
"@librechat/agents": "^2.4.12",
|
||||||
"@librechat/data-schemas": "*",
|
"@librechat/data-schemas": "*",
|
||||||
"@waylaidwanderer/fetch-event-source": "^3.0.1",
|
"@waylaidwanderer/fetch-event-source": "^3.0.1",
|
||||||
"axios": "^1.8.2",
|
"axios": "^1.8.2",
|
||||||
|
|
@ -17629,9 +17629,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@langchain/langgraph-sdk": {
|
"node_modules/@langchain/langgraph-sdk": {
|
||||||
"version": "0.0.62",
|
"version": "0.0.65",
|
||||||
"resolved": "https://registry.npmjs.org/@langchain/langgraph-sdk/-/langgraph-sdk-0.0.62.tgz",
|
"resolved": "https://registry.npmjs.org/@langchain/langgraph-sdk/-/langgraph-sdk-0.0.65.tgz",
|
||||||
"integrity": "sha512-JJiQwjV5/uVtwiVH/lt+QXhuh0nGhylZSLkMQXc1923TBUC4SHwU0JIKEDqh820PlGNkUu0nODJSAzS/6zPRtQ==",
|
"integrity": "sha512-Zn1FhiKr/mYa1+W5NcuCPWmdTtJS4UZYu+YVEjxgESd0aMX19FTkqjaSV6tFDcRqHHwlIgHloCSqHLkYWF/Zug==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/json-schema": "^7.0.15",
|
"@types/json-schema": "^7.0.15",
|
||||||
"p-queue": "^6.6.2",
|
"p-queue": "^6.6.2",
|
||||||
|
|
@ -17819,9 +17819,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@librechat/agents": {
|
"node_modules/@librechat/agents": {
|
||||||
"version": "2.4.0",
|
"version": "2.4.12",
|
||||||
"resolved": "https://registry.npmjs.org/@librechat/agents/-/agents-2.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/@librechat/agents/-/agents-2.4.12.tgz",
|
||||||
"integrity": "sha512-ijPJw+lMMPJ+Y66xSbh0cCiuODihl0TET3CWAxZweVGqynYqtL8PvPqlxtw+jftmrLjDFV04UM2NiDbzDb87HA==",
|
"integrity": "sha512-m8CEVCjVeQDKXMS0ISG4h4YXU1x51yWGPLCuiNbQI3k+fmDvEFqVuzFi15OhVfoGGQxbKfzGiiF8fsKK2xIKEw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@langchain/anthropic": "^0.3.16",
|
"@langchain/anthropic": "^0.3.16",
|
||||||
"@langchain/aws": "^0.1.7",
|
"@langchain/aws": "^0.1.7",
|
||||||
|
|
|
||||||
|
|
@ -112,7 +112,7 @@ export const excelMimeTypes =
|
||||||
/^application\/(vnd\.ms-excel|msexcel|x-msexcel|x-ms-excel|x-excel|x-dos_ms_excel|xls|x-xls|vnd\.openxmlformats-officedocument\.spreadsheetml\.sheet)$/;
|
/^application\/(vnd\.ms-excel|msexcel|x-msexcel|x-ms-excel|x-excel|x-dos_ms_excel|xls|x-xls|vnd\.openxmlformats-officedocument\.spreadsheetml\.sheet)$/;
|
||||||
|
|
||||||
export const textMimeTypes =
|
export const textMimeTypes =
|
||||||
/^(text\/(x-c|x-csharp|x-c\+\+|x-java|html|markdown|x-php|x-python|x-script\.python|x-ruby|x-tex|plain|css|vtt|javascript|csv))$/;
|
/^(text\/(x-c|x-csharp|tab-separated-values|x-c\+\+|x-java|html|markdown|x-php|x-python|x-script\.python|x-ruby|x-tex|plain|css|vtt|javascript|csv))$/;
|
||||||
|
|
||||||
export const applicationMimeTypes =
|
export const applicationMimeTypes =
|
||||||
/^(application\/(epub\+zip|csv|json|pdf|x-tar|typescript|vnd\.openxmlformats-officedocument\.(wordprocessingml\.document|presentationml\.presentation|spreadsheetml\.sheet)|xml|zip))$/;
|
/^(application\/(epub\+zip|csv|json|pdf|x-tar|typescript|vnd\.openxmlformats-officedocument\.(wordprocessingml\.document|presentationml\.presentation|spreadsheetml\.sheet)|xml|zip))$/;
|
||||||
|
|
@ -152,6 +152,7 @@ export const codeTypeMapping: { [key: string]: string } = {
|
||||||
yml: 'application/x-yaml',
|
yml: 'application/x-yaml',
|
||||||
yaml: 'application/x-yaml',
|
yaml: 'application/x-yaml',
|
||||||
log: 'text/plain',
|
log: 'text/plain',
|
||||||
|
tsv: 'text/tab-separated-values',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const retrievalMimeTypes = [
|
export const retrievalMimeTypes = [
|
||||||
|
|
@ -230,7 +231,7 @@ export const convertStringsToRegex = (patterns: string[]): RegExp[] =>
|
||||||
const regex = new RegExp(pattern);
|
const regex = new RegExp(pattern);
|
||||||
acc.push(regex);
|
acc.push(regex);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Invalid regex pattern "${pattern}" skipped.`);
|
console.error(`Invalid regex pattern "${pattern}" skipped.`, error);
|
||||||
}
|
}
|
||||||
return acc;
|
return acc;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue